diff --git a/.commitlintrc b/.commitlintrc index 73d886db1db..886c4606731 100644 --- a/.commitlintrc +++ b/.commitlintrc @@ -6,6 +6,7 @@ 2, "always", [ + "laravel", "symfony", "doctrine", "metadata", diff --git a/.gitattributes b/.gitattributes index 663af1c0b95..a2afb552166 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,7 +6,6 @@ .php-cs-fixer.dist.php export-ignore phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore -phpunit10.xml.dist export-ignore /.commitlintrc export-ignore /appveyor.yml export-ignore /behat.yml.dist export-ignore diff --git a/.github/workflows/api_platform.yml b/.github/workflows/api_platform.yml index ebf3e3c27dc..654fa98c005 100644 --- a/.github/workflows/api_platform.yml +++ b/.github/workflows/api_platform.yml @@ -1,10 +1,13 @@ -name: CI +name: Distribution update on: push: tags: - v* +env: + GH_TOKEN: ${{ github.token }} + concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1154c097cb6..6b4df6def02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,7 @@ jobs: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- + - run: composer validate - name: Update project dependencies run: | composer global require soyuka/pmu @@ -139,7 +140,6 @@ jobs: fail-fast: false env: APP_DEBUG: '1' # https://github.com/phpstan/phpstan-symfony/issues/37 - SYMFONY_PHPUNIT_VERSION: '9.5' steps: - name: Checkout uses: actions/checkout@v4 @@ -165,8 +165,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Cache PHPStan results uses: actions/cache@v4 with: @@ -184,6 +182,15 @@ jobs: run: | ./vendor/bin/phpstan --version ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi + - name: Install Laravel + working-directory: 'src/Laravel' + run: | + composer global link ../../ --working-directory=$(pwd) + composer run-script build + - name: Run PHPStan analysis (laravel) + working-directory: 'src/Laravel' + run: | + ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi phpunit: name: PHPUnit (PHP ${{ matrix.php }}) @@ -192,12 +199,9 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' include: - - php: '8.1' - coverage: true - php: '8.2' coverage: true - php: '8.3' @@ -228,12 +232,10 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml ${{ matrix.coverage && '--coverage-clover build/logs/phpunit/clover.xml' || '' }} + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml ${{ matrix.coverage && '--coverage-clover build/logs/phpunit/clover.xml' || '' }} - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 @@ -267,7 +269,6 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' component: @@ -275,20 +276,19 @@ jobs: - api-platform/doctrine-orm - api-platform/doctrine-odm - api-platform/metadata + - api-platform/hydra + - api-platform/json-api - api-platform/json-schema - api-platform/elasticsearch - api-platform/openapi - api-platform/graphql - api-platform/http-cache - - api-platform/parameter-validator - api-platform/ramsey-uuid - api-platform/serializer - api-platform/state - api-platform/symfony - api-platform/validator include: - - php: '8.1' - coverage: true - php: '8.2' coverage: true - php: '8.3' @@ -310,10 +310,6 @@ jobs: composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . --permanent composer ${{matrix.component}} update - - name: PHP version tweaks - if: matrix.component == 'api-platform/metadata' && matrix.php != '8.1' - run: composer require symfony/type-info - working-directory: 'src/Metadata' - name: Run ${{ matrix.component }} tests run: | mkdir -p /tmp/build/logs/phpunit @@ -351,16 +347,15 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' include: - - php: '8.1' - coverage: true - php: '8.2' coverage: true - php: '8.3' coverage: true + - php: '8.3' + coverage: false fail-fast: false steps: - name: Checkout @@ -387,8 +382,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests (PHP ${{ matrix.php }}) @@ -493,8 +486,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -546,8 +537,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -560,7 +549,6 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' fail-fast: false @@ -602,12 +590,12 @@ jobs: composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . --permanent composer require --dev doctrine/mongodb-odm-bundle - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mongodb + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --exclude-group=orm + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: | mkdir -p build/logs/behat @@ -650,7 +638,6 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' fail-fast: false @@ -696,12 +683,10 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure - name: Run Behat tests run: | mkdir -p build/logs/behat @@ -784,60 +769,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi - - name: Run Behat tests - run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction - - elasticsearch-lowest: - name: Behat (PHP ${{ matrix.php }}) (Elasticsearch 7) (Symfony lowest) - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - php: - - '8.3' - fail-fast: false - env: - APP_ENV: elasticsearch - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Configure sysctl limits - run: | - sudo swapoff -a - sudo sysctl -w vm.swappiness=1 - sudo sysctl -w fs.file-max=262144 - sudo sysctl -w vm.max_map_count=262144 - - name: Runs Elasticsearch - uses: elastic/elastic-github-actions/elasticsearch@master - with: - stack-version: '7.6.0' - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, mongodb - coverage: none - ini-values: memory_limit=-1 - - name: Get composer cache directory - id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Update project dependencies - run: | - composer global require soyuka/pmu - composer global config allow-plugins.soyuka/pmu true --no-interaction - composer global link . --permanent - composer update --prefer-lowest - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -877,12 +808,10 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit + run: vendor/bin/phpunit --fail-on-deprecation --display-deprecations phpunit-symfony-next: name: PHPUnit (PHP ${{ matrix.php }}) (Symfony dev) @@ -922,13 +851,10 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - # The PHPUnit Bridge doesn't support PHPUnit 10 yet: https://github.com/symfony/symfony/issues/49069 - run: vendor/bin/phpunit -c phpunit10.xml.dist + run: vendor/bin/phpunit behat-symfony-next: name: Behat (PHP ${{ matrix.php }}) (Symfony dev) @@ -970,74 +896,11 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction - windows-phpunit: - name: Windows PHPUnit (PHP ${{ matrix.php }}) (SQLite) - runs-on: windows-latest - timeout-minutes: 20 - strategy: - matrix: - php: - - '8.3' - fail-fast: false - env: - SYMFONY_PHPUNIT_VERSION: '9.5' - APP_ENV: sqlite - DATABASE_URL: sqlite:///%kernel.project_dir%/var/data.db - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup PHP with pre-release PECL extension - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite - coverage: none - ini-values: memory_limit=-1 - # Not in pecl - - name: Setup mongodb - run: | - curl -sLO https://github.com/mongodb/mongo-php-driver/releases/download/1.17.2/php_mongodb-1.17.2-8.3-nts-x64.zip - unzip -q php_mongodb-1.17.2-8.3-nts-x64.zip php_mongodb.dll - mv php_mongodb.dll C:\tools\php\ext - echo "extension=php_mongodb.dll" >> C:\tools\php\php.ini - - name: Get composer cache directory - id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - shell: bash - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Keep windows path - id: get-cwd - run: | - cwd=$(php -r 'echo(str_replace("\\", "\\\\", $_SERVER["argv"][1]));' '${{ github.workspace }}') - echo cwd=$cwd >> $GITHUB_OUTPUT - shell: bash - - name: Update project dependencies - run: | - composer global require soyuka/pmu - composer global config allow-plugins.soyuka/pmu true --no-interaction - composer global link . --working-directory='${{ steps.get-cwd.outputs.cwd }}' - - name: Install phpunit - run: vendor/bin/simple-phpunit --version - - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi - - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml - env: - SYMFONY_DEPRECATIONS_HELPER: max[direct]=0&ignoreFile=./tests/.ignored-deprecations - windows-behat: name: Windows Behat (PHP ${{ matrix.php }}) (SQLite) runs-on: windows-latest @@ -1048,7 +911,6 @@ jobs: - '8.3' fail-fast: false env: - SYMFONY_PHPUNIT_VERSION: '9.5' APP_ENV: sqlite DATABASE_URL: sqlite:///%kernel.project_dir%/var/data.db steps: @@ -1059,7 +921,7 @@ jobs: with: php-version: ${{ matrix.php }} tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, fileinfo coverage: none ini-values: memory_limit=-1 # Not in pecl @@ -1090,8 +952,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . --working-directory='${{ steps.get-cwd.outputs.cwd }}' - - name: Install phpunit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -1134,12 +994,10 @@ jobs: composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . --permanent composer update --prefer-lowest - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit + run: vendor/bin/phpunit env: SYMFONY_DEPRECATIONS_HELPER: max[self]=0&ignoreFile=./tests/.ignored-deprecations @@ -1187,10 +1045,10 @@ jobs: - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags='~@disableForSymfonyLowest' - phpunit_legacy: - name: PHPUnit Legacy event listeners (PHP ${{ matrix.php }}) + phpunit_listeners: + name: PHPUnit event listeners (PHP ${{ matrix.php }}) env: - EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER: 1 + USE_SYMFONY_LISTENERS: 1 runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -1229,19 +1087,15 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Use legacy ignored deprecations - run: cp tests/.ignored-deprecations-legacy-events tests/.ignored-deprecations - name: Run PHPUnit tests run: | mkdir -p build/logs/phpunit if [ "$COVERAGE" = '1' ]; then - vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml + vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml else - vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml + vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml fi - name: Upload test artifacts if: always() @@ -1269,78 +1123,6 @@ jobs: php-coveralls --coverage_clover=build/logs/phpunit/clover.xml continue-on-error: true - behat_legacy: - name: Behat Legacy event listeners (PHP ${{ matrix.php }}) - env: - EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER: 1 - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - php: - - '8.3' - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite - coverage: pcov - ini-values: memory_limit=-1 - - name: Get composer cache directory - id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Update project dependencies - run: | - composer global require soyuka/pmu - composer global config allow-plugins.soyuka/pmu true --no-interaction - composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi - - name: Run Behat tests (PHP 8) - run: | - mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=legacy --no-interaction - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: behat-logs-php${{ matrix.php }} - path: build/logs/behat - continue-on-error: true - - name: Export OpenAPI documents - run: | - mkdir -p build/out/openapi - tests/Fixtures/app/console api:openapi:export -o build/out/openapi/openapi_v3.json - tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: '14' - - name: Validate OpenAPI documents - run: | - npx swagger-cli validate build/out/openapi/openapi_v3.json - npx swagger-cli validate build/out/openapi/openapi_v3.yaml - - name: Upload OpenAPI artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: openapi-docs-php${{ matrix.php }} - path: build/out/openapi - continue-on-error: true - behat_listeners: name: Behat event listeners (PHP ${{ matrix.php }}) env: @@ -1377,8 +1159,6 @@ jobs: composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests (PHP 8) @@ -1413,16 +1193,20 @@ jobs: path: build/out/openapi continue-on-error: true - behat_legacy_query_parameter_validator: - name: Behat query parameter validator (PHP ${{ matrix.php }}) - env: - QUERY_PARAMETER_VALIDATOR: 1 + laravel: + name: Laravel (PHP ${{ matrix.php }}) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: + - '8.2' - '8.3' + include: + - php: '8.2' + coverage: true + - php: '8.3' + coverage: true fail-fast: false steps: - name: Checkout @@ -1432,35 +1216,17 @@ jobs: with: php-version: ${{ matrix.php }} tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite - coverage: pcov + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb ini-values: memory_limit=-1 - - name: Get composer cache directory - id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: | composer global require soyuka/pmu composer global config allow-plugins.soyuka/pmu true --no-interaction - composer global link . - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - - name: Clear test app cache - run: tests/Fixtures/app/console cache:clear --ansi - - name: Run Behat tests (PHP 8) + composer global link . --permanent + composer api-platform/laravel update + - name: PHP version tweaks run: | - mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --tags=query_parameter_validator - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: behat-logs-php${{ matrix.php }} - path: build/logs/behat - continue-on-error: true + composer run-script build + composer run-script test + working-directory: 'src/Laravel' + diff --git a/.github/workflows/subtree.yml b/.github/workflows/subtree.yml index 394f1c7cb1e..1e2969a9e3a 100644 --- a/.github/workflows/subtree.yml +++ b/.github/workflows/subtree.yml @@ -5,8 +5,11 @@ on: - v* branches: - main - - 3.1 - - 3.2 + - '3.1' + - '3.2' + - '3.3' + - '3.4' + - '4.0' env: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f790b3b5e99..15de4411c44 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,6 +15,8 @@ ->in(__DIR__) ->exclude([ 'src/Core/Bridge/Symfony/Maker/Resources/skeleton', + 'src/Laravel/Console/Maker/Resources/skeleton', + 'src/Laravel/config', 'tests/Fixtures/app/var', 'docs/guides', 'docs/var', @@ -22,14 +24,6 @@ 'src/Doctrine/Odm/Tests/var' ]) ->notPath('src/Symfony/Bundle/DependencyInjection/Configuration.php') - ->notPath('src/Annotation/ApiFilter.php') // temporary - ->notPath('src/Annotation/ApiProperty.php') // temporary - ->notPath('src/Annotation/ApiResource.php') // temporary - ->notPath('src/Annotation/ApiSubresource.php') // temporary - ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary - ->notPath('tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php') // PHPDoc on enum cases - ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases - ->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases ->append([ 'tests/Fixtures/app/console', ]); diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbd7c32d50..ffd5d418438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,184 @@ # Changelog +## v4.0.7 + +### Bug fixes + +* [17c916c3a](https://github.com/api-platform/core/commit/17c916c3a1bcc837c9bc842dc48390dbeb043450) fix(symfony): BackedEnumProvider typo fix (#6769) +* [216d9ccaa](https://github.com/api-platform/core/commit/216d9ccaacf7845daaaeab30f3a58bb5567430fe) fix(serializer): fetch type on normalization error when possible (#6761) +* [2f967d934](https://github.com/api-platform/core/commit/2f967d9345004779f409b9ce1b5d0cbba84c7132) fix(doctrine): throw an exception when a filter is not found in a parameter (#6767) +* [6c9b508b0](https://github.com/api-platform/core/commit/6c9b508b030976741a68f84e226502e6c218e896) fix(laravel): remove link header when jsonld is not enabled (#6768) +* [736ca045e](https://github.com/api-platform/core/commit/736ca045e6832f04aaa002ddd7b85c55df4696bb) fix(validator): allow to pass both a ConstraintViolationList and a previous exception (#6762) +* [9ac3661b6](https://github.com/api-platform/core/commit/9ac3661b6a75255832203b87a9ba7994add64061) fix(hydra): store and use hydra context in a local variable (#6765) + +### Features + +## v4.0.6 + +### Bug fixes + +* [195c4e788](https://github.com/api-platform/core/commit/195c4e7883520416e042ac78143b18652a216fbf) fix(hydra): hydra context changed (#6710) +* [4f65ef2d0](https://github.com/api-platform/core/commit/4f65ef2d061215df348e3505856f0f41c7c909ed) fix(metadata): providing parameter constraints skips automatic ones (#6756) +* [5a8ef115a](https://github.com/api-platform/core/commit/5a8ef115a90791992a6c1325fb6d1ac458b22153) fix(symfony): ECMA-262 pattern with RegExp validator (#6733) +* [67c5a2a24](https://github.com/api-platform/core/commit/67c5a2a2463bca94f0997b4fab1248a08994465b) fix(laravel): jsonapi error serialization (#6755) +* [ac6f667f3](https://github.com/api-platform/core/commit/ac6f667f301f6c4c399a707faf00567239bd98d8) fix(laravel): collection relations other than HasMany (#6737) + +### Features + +* [cecd77149](https://github.com/api-platform/core/commit/cecd77149795c1a455ac72bc3ed0606413e69900) feat(laravel): use laravel cache setting (#6751) + +## v4.0.5 + +### Bug fixes + +* [4171d5f9c](https://github.com/api-platform/core/commit/4171d5f9cd41731b857c53a186270ba0626baedf) fix(graphql): register query parameter arguments with filters (#6726) +* [48ab53816](https://github.com/api-platform/core/commit/48ab53816c55e6116aa64ac81f522f4b7b9bb9f6) fix(laravel): make command writes to app instead of src (#6723) + +### Features + +## v4.0.4 + +### Bug fixes + +* [2e8287dad](https://github.com/api-platform/core/commit/2e8287dad0c0315dd6527279a6359c0a22f40d93) fix(laravel): allow serializer attributes through ApiProperty (#6680) +* [439c188ea](https://github.com/api-platform/core/commit/439c188ea1685676d5e705a49a4b835f35a84d72) fix(laravel): match integer type (#6715) +* [4ad7a50aa](https://github.com/api-platform/core/commit/4ad7a50aaabf0d85e2eb5bb3a6d4ef8d5b7b39a7) fix(laravel): openapi Options binding (#6714) +* [ec6e64512](https://github.com/api-platform/core/commit/ec6e6451299a50fcab397e86fafe6db132ce7519) fix(laravel): skip resource path when not available (#6697) + +### Features + +* [5aa799321](https://github.com/api-platform/core/commit/5aa7993219a6fb55f11476a031963a542b2d3586) feat(laravel): command to generate state providers/processors (#6708) + +## v4.0.3 + +### Bug fixes + +* [025f63e69](https://github.com/api-platform/core/commit/025f63e69c2ec655a828559ed78c49a365ca043b) fix(laravel): route registration of EntrypointController should be last (#6667) +* [2b4937a3e](https://github.com/api-platform/core/commit/2b4937a3e09fb891b99fd8499b597190a4b740e0) fix(laravel): eloquent accessors (#6668) +* [4312a1f55](https://github.com/api-platform/core/commit/4312a1f55f4f80152be93734cb5cf73c70dee53a) fix(metadata): register parameters on graphql operations +* [6d4e24883](https://github.com/api-platform/core/commit/6d4e24883767f1c58dff5e52f57b0422110fa38f) fix(laravel): hiding/showing relationships (#6679) +* [85306f2f5](https://github.com/api-platform/core/commit/85306f2f5a7d480b1570471689d1d3ca4e9846a3) fix(laravel): swagger ui authentication (#6661) +* [a6e37068e](https://github.com/api-platform/core/commit/a6e37068ea49d1b5a4ee098a62a287d62fba1c35) fix(laravel): use Model::qualifyColumn instead of hardcoding $table.$column (#6658) +* [b0d5a2ade](https://github.com/api-platform/core/commit/b0d5a2adedb583074aa93d4f641bdda419d31ffa) fix(laravel): register global middleware to secure non-rest routes +* [f9d96e546](https://github.com/api-platform/core/commit/f9d96e546a37121244ab98d65c2d91f48b1bb112) fix(metadata): graphql can be disabled but with an existing operation + + +### Features + +* [df701da05](https://github.com/api-platform/core/commit/df701da05620a847f529ebabaee97f8cf5ecb37f) feat(laravel): graphql policies + +## v4.0.2 + +### Bug fixes + +* [219199db3](https://github.com/api-platform/core/commit/219199db386cab05f1c1225b889c0a9609b36699) fix(symfony): missing alias to serializer context builder interface (#6643) +* [5f943e3bb](https://github.com/api-platform/core/commit/5f943e3bb56934ba5d0b858f6b4c20a2985b6b6b) fix(graphql): wrong exception namespace (#6647) +* [72a0b669a](https://github.com/api-platform/core/commit/72a0b669a426ca4bbbf14cf80a6ced683b947e8c) fix(serializer): remove serializer context builder interface +* [88bd8c3e1](https://github.com/api-platform/core/commit/88bd8c3e151c843649bcac3feefc2cb956212410) fix(laravel): installation command, fix config overwrites (#6649) +* [93314b08d](https://github.com/api-platform/core/commit/93314b08de1e6f0505af9e3a3ba3d9971f1ef09c) fix(serializer): allow state's SerializerFilterContextBuilderInterface (#6632) +* [9a0afc917](https://github.com/api-platform/core/commit/9a0afc917a4bfa824ffbb640af9bb1114a5d31b4) fix(serializer): remove unnecessary dependency +* [c47e2996e](https://github.com/api-platform/core/commit/c47e2996e51c587c998fde88903703bd6ac9a43c) fix: default format and standard_put values +* [e327f5f69](https://github.com/api-platform/core/commit/e327f5f69c823c1ed674eefc0eb2551e30fb36bd) fix(symfony): namespace of path segment name generator services (#6642) + +Notes: + +`standard_put=true` is now the default, you can set it to `false` using `extra_properties.defaults` + +## v4.0.1 + +### Bug fixes + +* [eb80a1a56](https://github.com/api-platform/core/commit/eb80a1a5651b81cab13b018662a0d21e05facbfe) fix(state): precise format on content-location (#6627) + +### Features + +* [4a2271670](https://github.com/api-platform/core/commit/4a2271670a88c318ab38bd7eb2a1c0b93a5c0ea0) feat: api-platform/json-hal component (#6621) + +## v4.0.0 + +### Bug fixes + +* [7c5689626](https://github.com/api-platform/core/commit/7c568962634691892fe1057b3c982765a1c20ba2) fix(laravel): call authorize on delete but not validation (#6618) +* [d74b2b5fa](https://github.com/api-platform/core/commit/d74b2b5fa939a9f5ca11d3538e358070047a6c3d) fix: swagger ui with route identifier (#6616) +* [de6e3f546](https://github.com/api-platform/core/commit/de6e3f546a26c5ad5444d8e2448d81faec36bd73) fix(laravel): validate enum schema within filter (#6615) +* [0a461d749](https://github.com/api-platform/core/commit/0a461d749b7b4ac706f3b7b6138a13cb6e4a9d2d) fix(symfony): allow schema restriction for collection like property from choice constraint (#6520) +* [0c66a494d](https://github.com/api-platform/core/commit/0c66a494d3bda5817e59da2e43f0232e2e8fea15) fix(laravel): cache metadata, add trace on debug mode (#6555) +* [1b6e7c6cc](https://github.com/api-platform/core/commit/1b6e7c6ccf3d2c61816a7dceb35f1d5980ea0565) fix(laravel): disable GraphQL by default and fix provider +* [290944103](https://github.com/api-platform/core/commit/290944103039a4a6d64904d1a89264b800c809d5) fix(laravel): SwaggerUI title (#6527) +* [2df0860b5](https://github.com/api-platform/core/commit/2df0860b577bb1ae0882096436f3eaeb91281901) fix(laravel): Eloquent date and datetime type detection (#6529) +* [2fc74f2e6](https://github.com/api-platform/core/commit/2fc74f2e651f229e982343c1cb0c6a2c5d5eee64) fix: remove PUT from default operations (#6570) +* [3b42c9ff2](https://github.com/api-platform/core/commit/3b42c9ff235de5feac555d0283c513a6e4643953) fix: deserialization path for not denormalizable relations collected errors (#6537) +* [3c554a605](https://github.com/api-platform/core/commit/3c554a605ec9d5f36dfd852c4f93f0ce582064c9) fix(laravel): docs _format and open swagger ui (#6595) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [4ee209eff](https://github.com/api-platform/core/commit/4ee209effb0fd11789db9f016d6e90aa3cb942a9) fix(laravel): visible and hidden fields support (#6538) +* [6e15eb95f](https://github.com/api-platform/core/commit/6e15eb95fffb2deec3d381f5d6fd87e189772270) fix(laravel): register HydraPartialCollectionViewNormalizer (#6588) +* [86365be2a](https://github.com/api-platform/core/commit/86365be2a5b8e8d0050e09d4e401bb758aa8b7a8) fix(laravel): Eloquent PropertyAccessor (#6536) +* [a1dd0b54d](https://github.com/api-platform/core/commit/a1dd0b54d137d70f2163fa03690c1f4c74a549c0) fix(laravel): entrypoint with doc formats (#6552) +* [a4a53ab48](https://github.com/api-platform/core/commit/a4a53ab4838f4d17d3677952157b44ec165e3e3a) fix(laravel): identitifer is not writable unless marked as writable (#6531) +* [a6f355358](https://github.com/api-platform/core/commit/a6f355358a88e7cf7759db0dee41e185157ddc68) fix(laravel): do not normalize exception originalTrace (#6533) +* [bd6a57c4c](https://github.com/api-platform/core/commit/bd6a57c4c0b38d6f880a5bd79031a87033f707e6) fix(laravel): snake case props (#6532) +* [c31566602](https://github.com/api-platform/core/commit/c315666022185839742b8c5ef81601d85d8c3f4b) fix(laravel): api_doc route regex +* [ebc61d59d](https://github.com/api-platform/core/commit/ebc61d59d60eda3a020593cf4cb46c5d30548e46) fix(laravel): entrypoint serialization (#6541) + +### Features + +* [00787f32d](https://github.com/api-platform/core/commit/00787f32da54418de7d869cff218e22d8ae2ae1d) feat(laravel): automatically register policies (#6623) +* [06a647a80](https://github.com/api-platform/core/commit/06a647a80d4c6b7bfb3474d0685bcb445b56a5a8) feat(laravel): add CSV support (#6617) +* [a49bde1ea](https://github.com/api-platform/core/commit/a49bde1ea79ae4226b70c20f9bf967ac77e9ab89) feat(laravel): filter validations rules +* [03357fb90](https://github.com/api-platform/core/commit/03357fb90ac0003f0cec2002df01711d0fb99a1e) feat(laravel): supports more Eloquent types (#6544) +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [538648840](https://github.com/api-platform/core/commit/538648840dc7439b12033ed6f413f3167705da4d) feat(laravel): enable graphQl support (#6550) +* [5b9767be2](https://github.com/api-platform/core/commit/5b9767be215afdf61fccf8490d0a3a7018078ce5) feat(laravel): policy, auth and gate (#6523) +* [5e1233c57](https://github.com/api-platform/core/commit/5e1233c57a497d00a7c4bd3e3ad0cac25aeac014) feat(laravel): search filter (#6534) +* [9c461626f](https://github.com/api-platform/core/commit/9c461626f7d02f8d15487134b636f11966c19a5e) feat(laravel): provide a trait in addition to the annotation (#6543) +* [c9f18d4fb](https://github.com/api-platform/core/commit/c9f18d4fb833ea0b89ef18021cad491cf0600ef1) feat(laravel): eloquent filters (search, date, equals, or) (#6593) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v4.0.0-alpha.5 + +### Bug fixes + +* [0a461d749](https://github.com/api-platform/core/commit/0a461d749b7b4ac706f3b7b6138a13cb6e4a9d2d) fix(symfony): allow schema restriction for collection like property from choice constraint (#6520) +* [0c66a494d](https://github.com/api-platform/core/commit/0c66a494d3bda5817e59da2e43f0232e2e8fea15) fix(laravel): cache metadata, add trace on debug mode (#6555) +* [1b6e7c6cc](https://github.com/api-platform/core/commit/1b6e7c6ccf3d2c61816a7dceb35f1d5980ea0565) fix(laravel): disable GraphQL by default and fix provider +* [290944103](https://github.com/api-platform/core/commit/290944103039a4a6d64904d1a89264b800c809d5) fix(laravel): SwaggerUI title (#6527) +* [2df0860b5](https://github.com/api-platform/core/commit/2df0860b577bb1ae0882096436f3eaeb91281901) fix(laravel): Eloquent date and datetime type detection (#6529) +* [2fc74f2e6](https://github.com/api-platform/core/commit/2fc74f2e651f229e982343c1cb0c6a2c5d5eee64) fix: remove PUT from default operations (#6570) +* [3b42c9ff2](https://github.com/api-platform/core/commit/3b42c9ff235de5feac555d0283c513a6e4643953) fix: deserialization path for not denormalizable relations collected errors (#6537) +* [3c554a605](https://github.com/api-platform/core/commit/3c554a605ec9d5f36dfd852c4f93f0ce582064c9) fix(laravel): docs _format and open swagger ui (#6595) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [4ee209eff](https://github.com/api-platform/core/commit/4ee209effb0fd11789db9f016d6e90aa3cb942a9) fix(laravel): visible and hidden fields support (#6538) +* [6e15eb95f](https://github.com/api-platform/core/commit/6e15eb95fffb2deec3d381f5d6fd87e189772270) fix(laravel): register HydraPartialCollectionViewNormalizer (#6588) +* [86365be2a](https://github.com/api-platform/core/commit/86365be2a5b8e8d0050e09d4e401bb758aa8b7a8) fix(laravel): Eloquent PropertyAccessor (#6536) +* [a1dd0b54d](https://github.com/api-platform/core/commit/a1dd0b54d137d70f2163fa03690c1f4c74a549c0) fix(laravel): entrypoint with doc formats (#6552) +* [a4a53ab48](https://github.com/api-platform/core/commit/a4a53ab4838f4d17d3677952157b44ec165e3e3a) fix(laravel): identitifer is not writable unless marked as writable (#6531) +* [a6f355358](https://github.com/api-platform/core/commit/a6f355358a88e7cf7759db0dee41e185157ddc68) fix(laravel): do not normalize exception originalTrace (#6533) +* [bd6a57c4c](https://github.com/api-platform/core/commit/bd6a57c4c0b38d6f880a5bd79031a87033f707e6) fix(laravel): snake case props (#6532) +* [c31566602](https://github.com/api-platform/core/commit/c315666022185839742b8c5ef81601d85d8c3f4b) fix(laravel): api_doc route regex +* [ebc61d59d](https://github.com/api-platform/core/commit/ebc61d59d60eda3a020593cf4cb46c5d30548e46) fix(laravel): entrypoint serialization (#6541) + + +### Features + +* [03357fb90](https://github.com/api-platform/core/commit/03357fb90ac0003f0cec2002df01711d0fb99a1e) feat(laravel): supports more Eloquent types (#6544) +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [538648840](https://github.com/api-platform/core/commit/538648840dc7439b12033ed6f413f3167705da4d) feat(laravel): enable graphQl support (#6550) +* [5b9767be2](https://github.com/api-platform/core/commit/5b9767be215afdf61fccf8490d0a3a7018078ce5) feat(laravel): policy, auth and gate (#6523) +* [5e1233c57](https://github.com/api-platform/core/commit/5e1233c57a497d00a7c4bd3e3ad0cac25aeac014) feat(laravel): search filter (#6534) +* [9c461626f](https://github.com/api-platform/core/commit/9c461626f7d02f8d15487134b636f11966c19a5e) feat(laravel): provide a trait in addition to the annotation (#6543) +* [c9f18d4fb](https://github.com/api-platform/core/commit/c9f18d4fb833ea0b89ef18021cad491cf0600ef1) feat(laravel): eloquent filters (search, date, equals, or) (#6593) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v4.0.0-alpha.1 + +### Bug fixes + +* [f7f9f5427](https://github.com/api-platform/core/commit/f7f9f542719ad43d6e0fe74024a2fa05c11cb6fa) fix(laravel): phpstan/doc-parser is mandatory + +### Features + +* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882) + ## v3.4.5 ### Bug fixes @@ -2638,4 +2817,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd3da9b2217..9b74748781f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,18 +117,19 @@ Only the first commit on a Pull Request need to use a conventional commit, other ### Tests -On `api-platform/core` there are two kinds of tests: unit (`phpunit` through `simple-phpunit`) and integration tests (`behat`). +On `api-platform/core` there are two kinds of tests: unit (`phpunit`) and integration tests (`behat`). Note that we stopped using `prophesize` for new tests since 3.2, use `phpunit` stub system. -Both `simple-phpunit` and `behat` are development dependencies and should be available in the `vendor` directory. +Both `phpunit` and `behat` are development dependencies and should be available in the `vendor` directory. Recommendations: * don't change existing tests if possible * always add a new `ApiResource` or a new `Entity/Document` to add a new test instead of changing an existing class -* as of API Platform 3 each component has it's own test directory, avoid the `tests/` directory except for functional tests +* as of API Platform 3 each component has its own test directory, avoid the `tests/` directory except for functional tests * dependencies between components must be kept at its minimal (`api-platform/metadata`, `api-platform/state`) except for bridges (Doctrine, Symfony, Laravel etc.) +* for functional testing with phpunit (see `tests/Functional`, add your ApiResource to `ApiPlatform\Tests\Fixtures\PhpUnitResourceNameCollectionFactory`) Note that in most of the testing, you don't need Doctrine take a look at how we write fixtures at: @@ -138,11 +139,11 @@ https://github.com/api-platform/core/blob/002c8b25283c9c06a085945f6206052a99a5fb To launch unit tests: - vendor/bin/simple-phpunit --stop-on-defect -vvv + vendor/bin/phpunit --stop-on-defect If you want coverage, you will need the `pcov` PHP extension and run: - vendor/bin/simple-phpunit --coverage-html coverage -vvv --stop-on-failure + vendor/bin/phpunit --coverage-html coverage --stop-on-defect Sometimes there might be an error with too many open files when generating coverage. To fix this, you can increase the `ulimit`, for example: diff --git a/composer.json b/composer.json index 2f6b725b38c..39314dc5890 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,10 @@ "autoload": { "psr-4": { "ApiPlatform\\": "src/" - } + }, + "files": [ + "src/JsonLd/HydraContext.php" + ] }, "autoload-dev": { "psr-4": { @@ -38,8 +41,7 @@ "symfony/framework-bundle": "6.4.6 || 7.0.6", "symfony/var-exporter": "<6.1.1", "phpunit/phpunit": "<9.5", - "phpspec/prophecy": "<1.15", - "elasticsearch/elasticsearch": ">=8.0,<8.4" + "phpspec/prophecy": "<1.15" }, "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "extra": { @@ -71,7 +73,9 @@ "JSONAPI", "OpenAPI", "HAL", - "Swagger" + "Swagger", + "Symfony", + "Laravel" ], "license": "MIT", "name": "api-platform/core", @@ -99,103 +103,92 @@ "api-platform/validator": "self.version" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "doctrine/inflector": "^1.0 || ^2.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.4 || ^7.1", - "symfony/http-kernel": "^6.4 || ^7.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.1", - "willdurand/negotiation": "^3.0" + "symfony/web-link": "^6.4 || ^7.0", + "willdurand/negotiation": "^3.1" }, "require-dev": { - "api-platform/doctrine-common": "^3.4 || ^4.0", - "api-platform/doctrine-odm": "^3.4 || ^4.0", - "api-platform/doctrine-orm": "^3.4 || ^4.0", - "api-platform/documentation": "^3.4 || ^4.0", - "api-platform/elasticsearch": "^3.4 || ^4.0", - "api-platform/graphql": "^3.4 || ^4.0", - "api-platform/http-cache": "^3.4 || ^4.0", - "api-platform/hydra": "^3.4 || ^4.0", - "api-platform/json-api": "^3.3 || ^4.0", - "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/jsonld": "^3.4 || ^4.0", - "api-platform/metadata": "^3.4 || ^4.0", - "api-platform/openapi": "^3.4 || ^4.0", - "api-platform/parameter-validator": "^3.4", - "api-platform/ramsey-uuid": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/state": "^3.4 || ^4.0", - "api-platform/validator": "^3.4 || ^4.0", "behat/behat": "^3.11", "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", - "doctrine/dbal": "^3.4.0 || ^4.0", - "doctrine/doctrine-bundle": "^1.12 || ^2.0", - "doctrine/mongodb-odm": "^2.2", + "doctrine/dbal": "^4.0", + "doctrine/doctrine-bundle": "^2.11", + "doctrine/mongodb-odm": "^2.6", "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", - "doctrine/orm": "^2.14 || ^3.0", - "elasticsearch/elasticsearch": "^7.11 || ^8.4", + "doctrine/orm": "^2.17 || ^3.0", + "elasticsearch/elasticsearch": "^8.4", "friends-of-behat/mink-browserkit-driver": "^1.3.1", "friends-of-behat/mink-extension": "^2.2", "friends-of-behat/symfony-extension": "^2.1", - "guzzlehttp/guzzle": "^6.0 || ^7.1", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "illuminate/config": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/http": "^11.0", + "illuminate/pagination": "^11.0", + "illuminate/routing": "^11.0", + "illuminate/support": "^11.0", "jangregor/phpstan-prophecy": "^1.0", - "justinrainbow/json-schema": "^5.2.1", - "phpspec/prophecy-phpunit": "^2.0", + "justinrainbow/json-schema": "^5.2.11", + "laravel/framework": "^11.0", + "orchestra/testbench": "^9.1", + "phpspec/prophecy-phpunit": "^2.2", "phpstan/extension-installer": "^1.1", "phpstan/phpdoc-parser": "^1.13", "phpstan/phpstan": "^1.10", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^11.2", "psr/log": "^1.0 || ^2.0 || ^3.0", - "ramsey/uuid": "^3.9.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4 || ^2.0 || ^3.0", - "sebastian/comparator": "<5.0", - "soyuka/contexts": "v3.3.9", - "soyuka/pmu": "^0.0.12", + "ramsey/uuid": "^4.0", + "ramsey/uuid-doctrine": "^2.0", + "soyuka/contexts": "^3.3.10", + "soyuka/pmu": "^0.0.15", "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.4 || ^7.1", - "symfony/browser-kit": "^6.4 || ^7.1", - "symfony/cache": "^6.4 || ^7.1", - "symfony/config": "^6.4 || ^7.1", - "symfony/console": "^6.4 || ^7.1", - "symfony/css-selector": "^6.4 || ^7.1", - "symfony/dependency-injection": "^6.4 || ^7.1", - "symfony/doctrine-bridge": "^6.4 || ^7.1", - "symfony/dom-crawler": "^6.4 || ^7.1", - "symfony/error-handler": "^6.4 || ^7.1", - "symfony/event-dispatcher": "^6.4 || ^7.1", - "symfony/expression-language": "^6.4 || ^7.1", - "symfony/finder": "^6.4 || ^7.1", - "symfony/form": "^6.4 || ^7.1", - "symfony/framework-bundle": "^6.4 || ^7.1", - "symfony/http-client": "^6.4 || ^7.1", - "symfony/intl": "^6.4 || ^7.1", + "symfony/asset": "^6.4 || ^7.0", + "symfony/browser-kit": "^6.4 || ^7.0", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/css-selector": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/doctrine-bridge": "^6.4.2 || ^7.0.2", + "symfony/dom-crawler": "^6.4 || ^7.0", + "symfony/error-handler": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/form": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-client": "^6.4 || ^7.0", + "symfony/intl": "^6.4 || ^7.0", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.4 || ^7.1", - "symfony/phpunit-bridge": "^6.4.1 || ^7.1", - "symfony/routing": "^6.4 || ^7.1", - "symfony/security-bundle": "^6.4 || ^7.1", - "symfony/security-core": "^6.4 || ^7.1", - "symfony/stopwatch": "^6.4 || ^7.1", - "symfony/string": "^6.4 || ^7.1", - "symfony/twig-bundle": "^6.4 || ^7.1", - "symfony/uid": "^6.4 || ^7.1", - "symfony/validator": "^6.4 || ^7.1", - "symfony/web-profiler-bundle": "^6.4 || ^7.1", - "symfony/yaml": "^6.4 || ^7.1", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-core": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bundle": "^6.4 || ^7.0", + "symfony/uid": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", - "webonyx/graphql-php": "^14.0 || ^15.0" + "webonyx/graphql-php": "^15.0" }, "suggest": { "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", diff --git a/docs/adr/0005-refactor-state-management.md b/docs/adr/0005-refactor-state-management.md index a4196044dab..f5549e17d64 100644 --- a/docs/adr/0005-refactor-state-management.md +++ b/docs/adr/0005-refactor-state-management.md @@ -30,7 +30,7 @@ For API Platform 3, we refactored the whole metadata susbsytem to be more flexib This led to the refactoring of the two main interfaces allowing to plug a data source in API Platform: the state provider and the state processor interfaces. Leveraging these new interfaces, it should be possible to simplify the code base and to remove most code duplication by transforming most of the code currently -stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. +stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. This is quite close to what @alanpoulain proposed in 2019 at https://github.com/api-platform/core/pull/2978 although at that time we needed to refactor the subresource system before tackling this issue. ## Decision Outcome diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml index 3cc9e1ea7d3..e930a6e102d 100644 --- a/docs/config/packages/framework.yaml +++ b/docs/config/packages/framework.yaml @@ -7,13 +7,11 @@ api_platform: json: ['application/json'] docs_formats: jsonopenapi: ['application/vnd.openapi+json'] - keep_legacy_inflector: false http_cache: - invalidation: - enabled: true - public: true + invalidation: + enabled: true + public: true use_symfony_listeners: false defaults: extra_properties: - rfc_7807_compliant_errors: true standard_put: true diff --git a/docs/guides/create-a-custom-doctrine-filter.php b/docs/guides/create-a-custom-doctrine-filter.php index 32af2c2bf25..1c3c0192098 100644 --- a/docs/guides/create-a-custom-doctrine-filter.php +++ b/docs/guides/create-a-custom-doctrine-filter.php @@ -153,11 +153,11 @@ public function testAsAnonymousICanAccessTheDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection'); $this->assertJsonContains([ - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?regexp_title,regexp_author}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ + 'search' => [ + '@type' => 'IriTemplate', + 'template' => '/books.jsonld{?regexp_title,regexp_author}', + 'variableRepresentation' => 'BasicRepresentation', + 'mapping' => [ [ '@type' => 'IriTemplateMapping', 'variable' => 'regexp_title', diff --git a/docs/guides/custom-pagination.php b/docs/guides/custom-pagination.php index 85187f5c0c8..a83bac18724 100644 --- a/docs/guides/custom-pagination.php +++ b/docs/guides/custom-pagination.php @@ -7,7 +7,7 @@ // tags: expert // --- -// In case you're using a custom collection (through a Provider), make sure you return the `Paginator` object to get the full hydra response with `hydra:view` (which contains information about first, last, next and previous page). +// In case you're using a custom collection (through a Provider), make sure you return the `Paginator` object to get the full hydra response with `view` (which contains information about first, last, next and previous page). // // The following example shows how to handle it using a custom Provider. You will need to use the Doctrine Paginator and pass it to the API Platform Paginator. @@ -160,15 +160,15 @@ public function testTheCustomCollectionIsPaginated(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.'); + $this->assertNotSame(0, $response->toArray(false)['totalItems'], 'The collection is empty.'); $this->assertJsonContains([ - 'hydra:totalItems' => 35, - 'hydra:view' => [ + 'totalItems' => 35, + 'view' => [ '@id' => '/books.jsonld?page=1', - '@type' => 'hydra:PartialCollectionView', - 'hydra:first' => '/books.jsonld?page=1', - 'hydra:last' => '/books.jsonld?page=2', - 'hydra:next' => '/books.jsonld?page=2', + '@type' => 'PartialCollectionView', + 'first' => '/books.jsonld?page=1', + 'last' => '/books.jsonld?page=2', + 'next' => '/books.jsonld?page=2', ], ]); } diff --git a/docs/guides/delete-operation-with-validation.php b/docs/guides/delete-operation-with-validation.php index a8d285b85a9..017e73002fa 100644 --- a/docs/guides/delete-operation-with-validation.php +++ b/docs/guides/delete-operation-with-validation.php @@ -15,7 +15,7 @@ #[\Attribute] class AssertCanDelete extends Constraint { - public string $message = 'For whatever reason we denied removeal of this data.'; + public string $message = 'For whatever reason we denied removal of this data.'; public string $mode = 'strict'; public function getTargets(): string diff --git a/docs/guides/doctrine-search-filter.php b/docs/guides/doctrine-search-filter.php index d9f0cd21abb..bcf94c57336 100644 --- a/docs/guides/doctrine-search-filter.php +++ b/docs/guides/doctrine-search-filter.php @@ -118,21 +118,21 @@ public function testGetDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_books{._format}_get_collection', 'jsonld'); $this->assertJsonContains([ - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?author,title}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ + 'search' => [ + '@type' => 'IriTemplate', + 'template' => '/books.jsonld{?title,author}', + 'variableRepresentation' => 'BasicRepresentation', + 'mapping' => [ [ '@type' => 'IriTemplateMapping', - 'variable' => 'author', - 'property' => 'author', + 'variable' => 'title', + 'property' => 'title', 'required' => false, ], [ '@type' => 'IriTemplateMapping', - 'variable' => 'title', - 'property' => 'title', + 'variable' => 'author', + 'property' => 'author', 'required' => false, ], ], diff --git a/docs/guides/error-provider.php b/docs/guides/error-provider.php index 5a272ce4f4d..e417904008b 100644 --- a/docs/guides/error-provider.php +++ b/docs/guides/error-provider.php @@ -7,14 +7,6 @@ // tags: design, state // --- -// Note that we use the following configuration: -// ``` -// api_platform: -// defaults: -// rfc_7807_compliant_errors: true -// ``` -// To customize the API Platform response, replace the api_platform.state.error_provider with your own provider: - namespace App\ApiResource { use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; diff --git a/docs/guides/how-to.php b/docs/guides/how-to.php index d9999dac1a9..79aaa2a99c1 100644 --- a/docs/guides/how-to.php +++ b/docs/guides/how-to.php @@ -137,9 +137,9 @@ public function testAsAnonymousICanAccessTheDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.'); + $this->assertNotSame(0, $response->toArray(false)['totalItems'], 'The collection is empty.'); $this->assertJsonContains([ - 'hydra:totalItems' => 10, + 'totalItems' => 10, ]); } } diff --git a/docs/guides/return-the-iri-of-your-resources-relations.php b/docs/guides/return-the-iri-of-your-resources-relations.php index e8fec5cb639..41fb3b779a9 100644 --- a/docs/guides/return-the-iri-of-your-resources-relations.php +++ b/docs/guides/return-the-iri-of-your-resources-relations.php @@ -157,7 +157,7 @@ public function setBrand(Brand $brand): void // // The **OpenAPI** documentation will set the properties as `read-only` of type `string` in the format `iri-reference` for `JSON-LD`, `JSON:API` and `HAL` formats. // -// The **Hydra** documentation will set the properties as `hydra:Link` with the right domain, with `hydra:readable` to `true` but `hydra:writable` to `false`. +// The **Hydra** documentation will set the properties as `Link` with the right domain, with `readable` to `true` but `writable` to `false`. // // When using JSON:API or HAL formats, the IRI will be used and set links, embedded and relationship. // diff --git a/docs/guides/test-your-api.php b/docs/guides/test-your-api.php index 7749ccc920c..c0fea83ace5 100644 --- a/docs/guides/test-your-api.php +++ b/docs/guides/test-your-api.php @@ -39,7 +39,7 @@ public function testGetCollection(): void // matches an expected format, for example here with a collection. $this->assertMatchesResourceCollectionJsonSchema(Book::class); // PHPUnit default assertions are also available. - $this->assertCount(0, $response->toArray()['hydra:member']); + $this->assertCount(0, $response->toArray()['member']); } } } diff --git a/features/filter/filter_validation_legacy.feature b/features/filter/filter_validation_legacy.feature deleted file mode 100644 index cebfad23273..00000000000 --- a/features/filter/filter_validation_legacy.feature +++ /dev/null @@ -1,171 +0,0 @@ -@query_parameter_validator -Feature: Validate filters based upon filter description - - Background: - Given I add "Accept" header equal to "application/json" - - @createSchema - Scenario: Required filter should not throw an error if set - When I am on "/filter_validators?required=foo&required-allow-empty=&arrayRequired[foo]=" - Then the response status code should be 200 - - Scenario: Required filter that does not allow empty value should throw an error if empty - When I am on "/filter_validators?required=&required-allow-empty=&arrayRequired[foo]=" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "required" does not allow empty value' - - Scenario: Required filter should throw an error if not set - When I am on "/filter_validators" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "required" is required\nQuery parameter "required-allow-empty" is required$/' - - Scenario: Required filter should not throw an error if set - When I am on "/array_filter_validators?arrayRequired[]=foo&indexedArrayRequired[foo]=foo" - Then the response status code should be 200 - - Scenario: Required filter should throw an error if not set - When I am on "/array_filter_validators" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "arrayRequired\[\]" is required\nQuery parameter "indexedArrayRequired\[foo\]" is required$/' - - When I am on "/array_filter_validators?arrayRequired=foo&indexedArrayRequired[foo]=foo" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "arrayRequired[]" is required' - - When I am on "/array_filter_validators?arrayRequired[foo]=foo" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "arrayRequired\[\]" is required\nQuery parameter "indexedArrayRequired\[foo\]" is required$/' - - When I am on "/array_filter_validators?arrayRequired[]=foo" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "indexedArrayRequired[foo]" is required' - - When I am on "/array_filter_validators?arrayRequired[]=foo&indexedArrayRequired[bar]=bar" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "indexedArrayRequired[foo]" is required' - - Scenario: Test filter bounds: maximum - When I am on "/filter_validators?required=foo&required-allow-empty&maximum=10" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&maximum=11" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "maximum" must be less than or equal to 10' - - Scenario: Test filter bounds: exclusiveMaximum - When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMaximum=9" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMaximum=10" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "exclusiveMaximum" must be less than 10' - - Scenario: Test filter bounds: minimum - When I am on "/filter_validators?required=foo&required-allow-empty&minimum=5" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&minimum=0" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "minimum" must be greater than or equal to 5' - - Scenario: Test filter bounds: exclusiveMinimum - When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMinimum=6" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMinimum=5" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "exclusiveMinimum" must be greater than 5' - - Scenario: Test filter bounds: max length - When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3=123" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3=1234" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "max-length-3" length must be lower than or equal to 3' - - Scenario: Do not throw an error if value is not an array - When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3[]=12345" - Then the response status code should be 200 - - Scenario: Test filter bounds: min length - When I am on "/filter_validators?required=foo&required-allow-empty&min-length-3=123" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&min-length-3=12" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "min-length-3" length must be greater than or equal to 3' - - Scenario: Test filter pattern - When I am on "/filter_validators?required=foo&required-allow-empty&pattern=pattern" - When I am on "/filter_validators?required=foo&required-allow-empty&pattern=nrettap" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&pattern=not-pattern" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "pattern" must match pattern /^(pattern|nrettap)$/' - - Scenario: Test filter enum - When I am on "/filter_validators?required=foo&required-allow-empty&enum=in-enum" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&enum=not-in-enum" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "enum" must be one of "in-enum, mune-ni"' - - Scenario: Test filter multipleOf - When I am on "/filter_validators?required=foo&required-allow-empty&multiple-of=4" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&multiple-of=3" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "multiple-of" must multiple of 2' - - Scenario: Test filter array items csv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&csv-min-2=a,b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-min-2=a" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-min-2" must contain more than 2 values' - - Scenario: Test filter array items csv format maxItems - When I am on "/filter_validators?required=foo&required-allow-empty&csv-max-3=a,b,c" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-max-3=a,b,c,d" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-max-3" must contain less than 3 values' - - Scenario: Test filter array items tsv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&tsv-min-2=a\tb" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&tsv-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "tsv-min-2" must contain more than 2 values' - - Scenario: Test filter array items pipes format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&pipes-min-2=a|b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&pipes-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "pipes-min-2" must contain more than 2 values' - - Scenario: Test filter array items ssv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&ssv-min-2=a b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&ssv-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "ssv-min-2" must contain more than 2 values' - - @dropSchema - Scenario: Test filter array items unique items - When I am on "/filter_validators?required=foo&required-allow-empty&csv-uniques=a,b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-uniques=a,a" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-uniques" must contain unique values' diff --git a/features/hal/problem_legacy.feature b/features/hal/problem_legacy.feature deleted file mode 100644 index d2921884503..00000000000 --- a/features/hal/problem_legacy.feature +++ /dev/null @@ -1,50 +0,0 @@ -Feature: Error handling valid according to RFC 7807 (application/problem+json) - In order to be able to handle error client side - As a client software developer - I need to retrieve an RFC 7807 compliant serialization of errors - - Scenario: Get an error - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/json" - And I send a "POST" request to "/dummies" with body: - """ - {} - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10", - "title": "An error occurred", - "detail": "name: This value should not be blank.", - "violations": [ - { - "propertyPath": "name", - "message": "This value should not be blank.", - "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3" - } - ] - } - """ - - Scenario: Get an error during deserialization of simple relation - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummy": { - "name": "bar" - } - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10" - And the JSON node "title" should be equal to "An error occurred" - And the JSON node "detail" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist diff --git a/features/http_cache/headers.feature b/features/http_cache/headers.feature index b14b412bdbd..2fe42bd14c3 100644 --- a/features/http_cache/headers.feature +++ b/features/http_cache/headers.feature @@ -7,6 +7,6 @@ Feature: Default values of HTTP cache headers Scenario: Cache headers default value When I send a "GET" request to "/relation_embedders" Then the response status code should be 200 - And the header "Etag" should be equal to '"7bfa587950d675e222660f68623f5f89"' + And the header "Etag" should be equal to '"032297ac74d75a50"' And the header "Cache-Control" should be equal to "max-age=60, public, s-maxage=3600" And the header "Vary" should be equal to "Accept, Cookie" diff --git a/features/http_cache/tag_collector_service.feature b/features/http_cache/tag_collector_service.feature index 864ac7ee4be..ed994aadb7e 100644 --- a/features/http_cache/tag_collector_service.feature +++ b/features/http_cache/tag_collector_service.feature @@ -17,7 +17,7 @@ Feature: Cache invalidation through HTTP Cache tags (custom TagCollector service Then the response status code should be 201 And the header "Cache-Tags" should not exist - Scenario: TagCollector can identify $object (IRI is overriden with custom logic) + Scenario: TagCollector can identify $object (IRI is overridden with custom logic) When I send a "GET" request to "/relation_embedders/1" Then the response status code should be 200 And the header "Cache-Tags" should be equal to "/RE/1#anotherRelated,/RE/1#related,/RE/1" @@ -126,7 +126,7 @@ Feature: Cache invalidation through HTTP Cache tags (custom TagCollector service Then the response status code should be 201 And the header "Cache-Tags" should not exist - Scenario: TagCollector can read propertyMetadata (tag is overriden with data from extraProperties) + Scenario: TagCollector can read propertyMetadata (tag is overridden with data from extraProperties) When I send a "GET" request to "/extra_properties_on_properties/1" Then the response status code should be 200 And the header "Cache-Tags" should be equal to "/extra_properties_on_properties/1#overrideRelationTag,/extra_properties_on_properties/1" diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index 2ba5de7f1b3..399c7af4bb6 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -77,6 +77,7 @@ Feature: Collections support When I send a "GET" request to "/dummies?page=7" Then the response status code should be 200 And the response should be in JSON + And the header "Content-Location" should be equal to "/dummies.jsonld?page=7" And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" And the JSON should be valid according to this schema: """ @@ -479,7 +480,7 @@ Feature: Collections support When I send a "GET" request to "/dummies?itemsPerPage=0&page=2" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Page should not be greater than 1 if limit is equal to 0" + And the JSON node "description" should be equal to "Page should not be greater than 1 if limit is equal to 0" Scenario: Cursor-based pagination with an empty collection When I send a "GET" request to "/so_manies" diff --git a/features/hydra/docs.feature b/features/hydra/docs.feature index d848ac55969..3b5d665a2c6 100644 --- a/features/hydra/docs.feature +++ b/features/hydra/docs.feature @@ -13,22 +13,19 @@ Feature: Documentation support And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" # Context - And the JSON node "@context.@vocab" should be equal to "http://example.com/docs.jsonld#" - And the JSON node "@context.hydra" should be equal to "http://www.w3.org/ns/hydra/core#" - And the JSON node "@context.rdf" should be equal to "http://www.w3.org/1999/02/22-rdf-syntax-ns#" - And the JSON node "@context.rdfs" should be equal to "http://www.w3.org/2000/01/rdf-schema#" - And the JSON node "@context.xmls" should be equal to "http://www.w3.org/2001/XMLSchema#" - And the JSON node "@context.owl" should be equal to "http://www.w3.org/2002/07/owl#" - And the JSON node "@context.domain.@id" should be equal to "rdfs:domain" - And the JSON node "@context.domain.@type" should be equal to "@id" - And the JSON node "@context.range.@id" should be equal to "rdfs:range" - And the JSON node "@context.range.@type" should be equal to "@id" - And the JSON node "@context.subClassOf.@id" should be equal to "rdfs:subClassOf" - And the JSON node "@context.subClassOf.@type" should be equal to "@id" - And the JSON node "@context.expects.@id" should be equal to "hydra:expects" - And the JSON node "@context.expects.@type" should be equal to "@id" - And the JSON node "@context.returns.@id" should be equal to "hydra:returns" - And the JSON node "@context.returns.@type" should be equal to "@id" + And the Hydra context matches the online resource "http://www.w3.org/ns/hydra/context.jsonld" + And the JSON node "@context[1].@vocab" should be equal to "http://example.com/docs.jsonld#" + And the JSON node "@context[1].hydra" should be equal to "http://www.w3.org/ns/hydra/core#" + And the JSON node "@context[1].rdf" should be equal to "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + And the JSON node "@context[1].rdfs" should be equal to "http://www.w3.org/2000/01/rdf-schema#" + And the JSON node "@context[1].xmls" should be equal to "http://www.w3.org/2001/XMLSchema#" + And the JSON node "@context[1].owl" should be equal to "http://www.w3.org/2002/07/owl#" + And the JSON node "@context[1].domain.@id" should be equal to "rdfs:domain" + And the JSON node "@context[1].domain.@type" should be equal to "@id" + And the JSON node "@context[1].range.@id" should be equal to "rdfs:range" + And the JSON node "@context[1].range.@type" should be equal to "@id" + And the JSON node "@context[1].subClassOf.@id" should be equal to "rdfs:subClassOf" + And the JSON node "@context[1].subClassOf.@type" should be equal to "@id" # Root properties And the JSON node "@id" should be equal to "/docs.jsonld" And the JSON node "hydra:title" should be equal to "My Dummy API" @@ -79,7 +76,7 @@ Feature: Documentation support And the value of the node "hydra:method" of the operation "GET" of the Hydra class "Dummy" is "GET" And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." And the value of the node "rdfs:label" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." - And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "#Dummy" + And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "Dummy" And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource." And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource." And the value of the node "returns" of the operation "DELETE" of the Hydra class "Dummy" is "owl:Nothing" diff --git a/features/hydra/error.feature b/features/hydra/error.feature index 1663597c768..d43ea714cb5 100644 --- a/features/hydra/error.feature +++ b/features/hydra/error.feature @@ -23,7 +23,7 @@ Feature: Error handling And the JSON node "hydra:description" should exist And the JSON node "trace" should exist And the JSON node "status" should exist - And the JSON node "@context" should not exist + And the JSON node "@context" should exist Scenario: Get validation constraint violations When I add "Content-Type" header equal to "application/ld+json" @@ -37,6 +37,7 @@ Feature: Error handling And the JSON should be equal to: """ { + "@context": "/contexts/ConstraintViolationList", "@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3", "@type": "ConstraintViolationList", "status": 422, @@ -66,7 +67,7 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist And the JSON node "title" should be equal to "An error occurred" And the JSON node "detail" should exist @@ -81,7 +82,7 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist And the JSON node "title" should be equal to "An error occurred" And the JSON node "detail" should exist @@ -98,7 +99,7 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist And the JSON node "title" should be equal to "An error occurred" And the JSON node "detail" should exist @@ -115,7 +116,7 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist And the JSON node "title" should be equal to "An error occurred" And the JSON node "detail" should exist @@ -137,4 +138,3 @@ Feature: Error handling And the JSON node "description" should exist And the JSON node "trace" should exist And the JSON node "status" should exist - And the JSON node "@context" should not exist diff --git a/features/hydra/error_legacy.feature b/features/hydra/error_legacy.feature deleted file mode 100644 index 9b97476e315..00000000000 --- a/features/hydra/error_legacy.feature +++ /dev/null @@ -1,165 +0,0 @@ -Feature: Error handling - In order to be able to handle error client side - As a client software developer - I need to retrieve an Hydra serialization of errors - - Scenario: Get an error - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - {} - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "name: This value should not be blank.", - "violations": [ - { - "propertyPath": "name", - "message": "This value should not be blank.", - "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3" - } - ] - } - """ - - Scenario: Get an error during deserialization of simple relation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummy": { - "name": "bar" - } - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist - - Scenario: Get an error during deserialization of collection - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummies": [{ - "name": "bar" - }] - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Nested documents for attribute "relatedDummies" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist - - Scenario: Get an error because of an invalid JSON - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should exist - And the JSON node "trace" should exist - - Scenario: Get an error during update of an existing resource with a non-allowed update operation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "@id": "/dummies/1", - "name": "Foo" - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Update is not allowed for this operation." - And the JSON node "trace" should exist - - @createSchema - Scenario: Populate database with related dummies. Check that id will be "/related_dummies/1" - Given I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/related_dummies" with body: - """ - { - "@type": "https://schema.org/Product", - "symfony": "laravel" - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the JSON node "@id" should be equal to "/related_dummies/1" - And the JSON node "symfony" should be equal to "laravel" - - Scenario: Do not get an error during update of an existing relation with a non-allowed update operation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/relation_embedders" with body: - """ - { - "anotherRelated": { - "@id": "/related_dummies/1", - "@type": "https://schema.org/Product", - "symfony": "phalcon" - } - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/RelationEmbedder" - And the JSON node "@type" should be equal to "RelationEmbedder" - And the JSON node "@id" should be equal to "/relation_embedders/1" - And the JSON node "anotherRelated.@id" should be equal to "/related_dummies/1" - And the JSON node "anotherRelated.symfony" should be equal to "phalcon" - - Scenario: Get an error because of sending bad type property - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/ld+json" - And I send a "POST" request to "/greetings" with body: - """ - { - "0": 1 - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - - Scenario: Get an rfc 7807 error with backward compatibility - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/exception_problems_with_compatibility" with body: - """ - {} - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should exist diff --git a/features/jsonapi/errors_legacy.feature b/features/jsonapi/errors_legacy.feature deleted file mode 100644 index 587ee74a837..00000000000 --- a/features/jsonapi/errors_legacy.feature +++ /dev/null @@ -1,73 +0,0 @@ -Feature: JSON API error handling - In order to be able to handle error client side - As a client software developer - I need to retrieve an JSON API serialization of errors - - Background: - Given I add "Accept" header equal to "application/vnd.api+json" - And I add "Content-Type" header equal to "application/vnd.api+json" - - @createSchema - Scenario: Get a validation error on an attribute - When I send a "POST" request to "/dummies" with body: - """ - { - "data": { - "type": "dummy", - "attributes": {} - } - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the JSON should be valid according to the JSON API schema - And the JSON should be equal to: - """ - { - "errors": [ - { - "detail": "This value should not be blank.", - "source": { - "pointer": "data/attributes/name" - } - } - ] - } - """ - - Scenario: Get a validation error on an relationship - Given there is a RelatedDummy - And there is a DummyFriend - When I send a "POST" request to "/related_to_dummy_friends" with body: - """ - { - "data": { - "type": "RelatedToDummyFriend", - "attributes": { - "name": "Related to dummy friend" - } - } - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the JSON should be valid according to the JSON API schema - And the JSON should be equal to: - """ - { - "errors": [ - { - "detail": "This value should not be null.", - "source": { - "pointer": "data/relationships/dummyFriend" - } - }, - { - "detail": "This value should not be null.", - "source": { - "pointer": "data/relationships/relatedDummy" - } - } - ] - } - """ diff --git a/features/jsonld/interface_as_resource.feature b/features/jsonld/interface_as_resource.feature index 00e0224b149..7236ace7ae7 100644 --- a/features/jsonld/interface_as_resource.feature +++ b/features/jsonld/interface_as_resource.feature @@ -15,7 +15,7 @@ Feature: JSON-LD using interface as resource "code": "WONDERFUL_TAXON" } """ - When I send a "GET" request to "/taxons/WONDERFUL_TAXON" + When I send a "GET" request to "/taxa/WONDERFUL_TAXON" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -23,7 +23,7 @@ Feature: JSON-LD using interface as resource """ { "@context": "/contexts/Taxon", - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } @@ -49,7 +49,7 @@ Feature: JSON-LD using interface as resource "@type": "Product", "code": "GREAT_PRODUCT", "mainTaxon": { - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } diff --git a/features/main/crud.feature b/features/main/crud.feature index 1fc310d63bb..8dc25500ad2 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -22,7 +22,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/dummies/1" + And the header "Content-Location" should be equal to "/dummies/1.jsonld" And the header "Location" should be equal to "/dummies/1" And the JSON should be equal to: """ @@ -95,7 +95,7 @@ Feature: Create-Retrieve-Update-Delete When I add "Content-Type" header equal to "application/ld+json" And I send a "POST" request to "/dummies" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Syntax error" + And the JSON node "description" should be equal to "Syntax error" Scenario: Get a not found exception When I send a "GET" request to "/dummies/42" @@ -106,6 +106,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should be equal to "/dummies.jsonld" And the JSON should be equal to: """ { @@ -513,7 +514,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/dummies/1" + And the header "Content-Location" should be equal to "/dummies/1.jsonld" And the JSON should be equal to: """ { @@ -551,7 +552,7 @@ Feature: Create-Retrieve-Update-Delete When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/dummies/1" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Syntax error" + And the JSON node "description" should be equal to "Syntax error" Scenario: Delete a resource When I send a "DELETE" request to "/dummies/1" @@ -571,7 +572,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/processor_entities/1" + And the header "Content-Location" should be equal to "/processor_entities/1.jsonld" And the header "Location" should be equal to "/processor_entities/1" And the JSON should be equal to: """ @@ -596,7 +597,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/provider_entities/1" + And the header "Content-Location" should be equal to "/provider_entities/1.jsonld" And the header "Location" should be equal to "/provider_entities/1" And the JSON should be equal to: """ diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature index 87d6ca2578f..12d99f397eb 100644 --- a/features/main/crud_abstract.feature +++ b/features/main/crud_abstract.feature @@ -16,7 +16,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the header "Location" should be equal to "/concrete_dummies/1" And the JSON should be equal to: """ @@ -92,7 +92,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -118,7 +118,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -150,7 +150,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the header "Location" should be equal to "/concrete_dummies/1" And the JSON should be equal to: """ diff --git a/features/main/crud_uri_variables.feature b/features/main/crud_uri_variables.feature index fb86d203961..68aef4a3173 100644 --- a/features/main/crud_uri_variables.feature +++ b/features/main/crud_uri_variables.feature @@ -13,7 +13,7 @@ Feature: Uri Variables Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/companies/1" + And the header "Content-Location" should be equal to "/companies/1.jsonld" And the header "Location" should be equal to "/companies/1" And the JSON should be equal to: """ @@ -51,7 +51,7 @@ Feature: Uri Variables Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/companies/1/employees/1" + And the header "Content-Location" should be equal to "/companies/1/employees/1.jsonld" And the header "Location" should be equal to "/companies/1/employees/1" And the JSON should be equal to: """ diff --git a/features/main/custom_normalized.feature b/features/main/custom_normalized.feature index 67a668fa0f0..7d1a49f3fed 100644 --- a/features/main/custom_normalized.feature +++ b/features/main/custom_normalized.feature @@ -16,7 +16,7 @@ Feature: Using custom normalized entity Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the header "Location" should be equal to "/custom_normalized_dummies/1" And the JSON should be equal to: """ @@ -43,7 +43,7 @@ Feature: Using custom normalized entity Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the header "Content-Location" should be equal to "/related_normalized_dummies/1" + And the header "Content-Location" should be equal to "/related_normalized_dummies/1.json" And the header "Location" should be equal to "/related_normalized_dummies/1" And the JSON should be equal to: """ @@ -92,7 +92,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the header "Content-Location" should be equal to "/related_normalized_dummies/1" + And the header "Content-Location" should be equal to "/related_normalized_dummies/1.json" And the JSON should be equal to: """ { @@ -158,7 +158,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -182,7 +182,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/custom_writable_identifier.feature b/features/main/custom_writable_identifier.feature index d2878f0afc5..097d253b9ad 100644 --- a/features/main/custom_writable_identifier.feature +++ b/features/main/custom_writable_identifier.feature @@ -16,7 +16,7 @@ Feature: Using custom writable identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug" + And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug.jsonld" And the header "Location" should be equal to "/custom_writable_identifier_dummies/my_slug" And the JSON should be equal to: """ @@ -81,7 +81,7 @@ Feature: Using custom writable identifier on resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified" + And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/not_exposed.feature b/features/main/not_exposed.feature index 09f4d8691d9..5e6173052f6 100644 --- a/features/main/not_exposed.feature +++ b/features/main/not_exposed.feature @@ -171,7 +171,7 @@ Feature: Expose only a collection of objects When I send a "GET" request to "" Then the response status code should be 404 And the response should be in JSON - And the JSON node "hydra:description" should be equal to "" + And the JSON node "description" should be equal to "" Examples: | uri | description | | /tables/12345 | This route does not aim to be called. | diff --git a/features/main/operation_resource.feature b/features/main/operation_resource.feature index 070cb5be9fd..b4bd729fcaf 100644 --- a/features/main/operation_resource.feature +++ b/features/main/operation_resource.feature @@ -53,7 +53,7 @@ Feature: Resource operations Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/operation_resources/1" + And the header "Content-Location" should be equal to "/operation_resources/1.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/uuid.feature b/features/main/uuid.feature index d0634b0bc94..a7506a15bec 100644 --- a/features/main/uuid.feature +++ b/features/main/uuid.feature @@ -16,7 +16,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" + And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78.jsonld" And the header "Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" Scenario: Get a resource @@ -69,7 +69,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" + And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78.jsonld" And the JSON should be equal to: """ { @@ -90,7 +90,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo" + And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo.jsonld" And the header "Location" should be equal to "/custom_generated_identifiers/foo" And the JSON should be equal to: """ diff --git a/features/main/validation.feature b/features/main/validation.feature index 209d4fe2bc5..9dabd131bdd 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -26,13 +26,13 @@ Feature: Using validations groups """ Then the response status code should be 422 And the response should be in JSON - And the JSON should be equal to: + And the JSON should be a superset of: """ { "@context": "/contexts/ConstraintViolationList", "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "name: This value should not be null.", + "title": "An error occurred", + "description": "name: This value should not be null.", "violations": [ { "propertyPath": "name", @@ -42,7 +42,7 @@ Feature: Using validations groups ] } """ - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" @createSchema Scenario: Create a resource with validation group sequence @@ -55,13 +55,13 @@ Feature: Using validations groups """ Then the response status code should be 422 And the response should be in JSON - And the JSON should be equal to: + And the JSON should be a superset of: """ { "@context": "/contexts/ConstraintViolationList", "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "title: This value should not be null.", + "title": "An error occurred", + "description": "title: This value should not be null.", "violations": [ { "propertyPath": "title", @@ -71,7 +71,7 @@ Feature: Using validations groups ] } """ - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" @createSchema Scenario: Create a resource with serializedName property @@ -107,8 +107,8 @@ Feature: Using validations groups """ Then the response status code should be 422 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + And the JSON should be a superset of: """ { "@context": "/contexts/ConstraintViolationList", diff --git a/features/mongodb/filters.feature b/features/mongodb/filters.feature index 9e89239215d..7658ed5c322 100644 --- a/features/mongodb/filters.feature +++ b/features/mongodb/filters.feature @@ -10,20 +10,20 @@ Feature: Filters on collections When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4" Then the response status code should be 500 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported." And the JSON node "trace" should exist Scenario: Error when getting collection with nested properties if references are not correctly stored (not owning side) When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3" Then the response status code should be 500 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported." And the JSON node "trace" should exist diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 980b896959a..5d5d802c1dc 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -438,3 +438,9 @@ Feature: Documentation support And I send a "GET" request to "/docs.jsonopenapi" Then the response status code should be 200 And the response should be in JSON + + Scenario: OpenAPI UI is enabled for docs endpoint + Given there are 1 dummy objects + Given I add "Accept" header equal to "text/html" + And I send a "GET" request to "/dummies/1.html" + Then the response status code should be 200 diff --git a/features/security/send_security_headers.feature b/features/security/send_security_headers.feature index 7d38893bf01..bca2f6fb9c1 100644 --- a/features/security/send_security_headers.feature +++ b/features/security/send_security_headers.feature @@ -27,6 +27,6 @@ Feature: Send security header {"name": ""} """ Then the response status code should be 422 - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "X-Content-Type-Options" should be equal to "nosniff" And the header "X-Frame-Options" should be equal to "deny" diff --git a/features/security/strong_typing.feature b/features/security/strong_typing.feature index 3d4b7599a62..66506548717 100644 --- a/features/security/strong_typing.feature +++ b/features/security/strong_typing.feature @@ -52,11 +52,11 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.' + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.' Scenario: Create a resource with wrong value type for relation When I add "Content-Type" header equal to "application/ld+json" @@ -69,11 +69,11 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Invalid IRI "1".' + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to 'Invalid IRI "1".' And the JSON node "trace" should exist Scenario: Ignore invalid dates @@ -87,7 +87,7 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" Scenario: Send non-array data when an array is expected When I add "Content-Type" header equal to "application/ld+json" @@ -100,11 +100,11 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.' + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.' And the JSON node "trace" should exist Scenario: Send an object where an array is expected @@ -118,11 +118,11 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the key "a" must be "int", "string" given.' + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to 'The type of the key "a" must be "int", "string" given.' Scenario: Send a scalar having the bad type When I add "Content-Type" header equal to "application/ld+json" @@ -134,11 +134,11 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "name" attribute must be "string", "integer" given.' + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "description" should be equal to 'The type of the "name" attribute must be "string", "integer" given.' Scenario: According to the JSON spec, allow numbers without explicit floating point for JSON formats When I add "Content-Type" header equal to "application/ld+json" diff --git a/features/security/validate_incoming_content-types.feature b/features/security/validate_incoming_content-types.feature index 993352428cc..27a75000822 100644 --- a/features/security/validate_incoming_content-types.feature +++ b/features/security/validate_incoming_content-types.feature @@ -13,5 +13,5 @@ Feature: Validate incoming content type something """ Then the response status code should be 415 - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "hydra:description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".' + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + And the JSON node "description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".' diff --git a/generate-changelog.sh b/generate-changelog.sh index f2903beb1b3..bf5cb10ec43 100755 --- a/generate-changelog.sh +++ b/generate-changelog.sh @@ -1,23 +1,26 @@ #!/bin/bash # usage: generate-changelog.sh previous_tag next_tag # example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md -log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) +lowerbranch=$(git branch --merged HEAD | grep '[[:digit:]]\.[[:digit:]]' | grep -v '*' | sort -rg | head -n 1) +log=$(git log "$1..HEAD" --no-merges --not $lowerbranch --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s') diff=$( printf "# Changelog\n\n" printf "## %s\n\n" "$2" -if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; +fixes=$(echo "$log" | grep 'fix(\|fix:') +if [[ 0 != $(echo "$fixes" | wc -l) ]]; then printf "### Bug fixes\n\n" - printf "$log" | grep fix | grep -v chore | sort + printf "$fixes" | sort printf "\n\n" fi -if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; +feat=$(echo "$log" | grep 'feat(\|feat:') +if [[ 0 != $(echo "$feat" | wc -l) ]]; then printf "### Features\n\n" - printf "$log" | grep feat | grep -v chore | sort + printf "$feat" | sort fi ) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2dfec535ad9..d1a6aa0bb23 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,11 +11,12 @@ parameters: doctrine: objectManagerLoader: tests/Fixtures/app/object-manager.php bootstrapFiles: - - vendor/bin/.phpunit/phpunit/vendor/autoload.php # We're aliasing classes for phpunit in this file, it needs to be added here see phpstan/#2194 - src/Symfony/Bundle/Test/Constraint/ArraySubset.php - tests/Fixtures/app/AppKernel.php excludePaths: + # uses larastan + - src/Laravel - src/Symfony/Bundle/Command/OpenApiCommand.php # Symfony config - tests/Fixtures/app/config/config_swagger.php @@ -65,15 +66,7 @@ parameters: message: '#Parameter \#1 \$constraint of method#' paths: - tests/Symfony/Validator/Metadata/Property/Restriction/ - - - message: '#expects ApiPlatform\\Metadata\\GraphQl\\Operation\|null, ApiPlatform\\Metadata\\Operation given#' - paths: - - src/GraphQl/Tests/Resolver/Factory/ - '#Access to an undefined property Prophecy\\Prophecy\\ObjectProphecy<(\\?[a-zA-Z0-9_]+)+>::\$[a-zA-Z0-9_]+#' - # https://github.com/willdurand/Negotiation/issues/89#issuecomment-513283286 - - - message: '#Call to an undefined method Negotiation\\AcceptHeader::getType\(\)\.#' - path: src/Symfony/EventListener/AddFormatListener.php # https://github.com/phpstan/phpstan-symfony/issues/76 - message: '#Service "test" is not registered in the container\.#' @@ -104,10 +97,4 @@ parameters: - message: '#^Service "[^"]+" is private.$#' path: src - - - message: '#^Class .+ not found.$#' - path: src/Elasticsearch/Tests - # Backward compatibility - - '#Call to method hasCacheableSupportsMethod\(\) on an unknown class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface\.#' - - '#Class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface not found\.#' - - '#Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist.#' + - '#Call to an undefined method Symfony\\Component\\HttpFoundation\\Request::getContentType\(\)\.#' diff --git a/phpunit.baseline.xml b/phpunit.baseline.xml new file mode 100644 index 00000000000..d72b0ee6d7c --- /dev/null +++ b/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 230919fb769..7d28d8a16a0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,50 +1,41 @@ + + + + + + + + + + + - - - - - - - - - - - - - - - + + + tests + + - - - tests - - + + + mongodb + mercure + + - - - src - - - src/Symfony/Maker/Resources/skeleton - features - tests - vendor - src/**/Tests/ - src/Doctrine/**/Tests/ - .php-cs-fixer.dist.php - - - - - - - - - - mongodb - mercure - - + + + . + + + features + vendor + .php-cs-fixer.dist.php + + diff --git a/phpunit10.xml.dist b/phpunit10.xml.dist deleted file mode 100644 index 9c8c1d2588a..00000000000 --- a/phpunit10.xml.dist +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - tests - - - - - - mongodb - mercure - - - - - - . - - - features - tests - vendor - .php-cs-fixer.dist.php - - - diff --git a/src/Action/EntrypointAction.php b/src/Action/EntrypointAction.php deleted file mode 100644 index 94cf2a5e1a0..00000000000 --- a/src/Action/EntrypointAction.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\ProviderInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * Generates the API entrypoint. - * - * @deprecated use ApiPlatform\Documentation\Action\EntrypointAction - * - * @author Kévin Dunglas - */ -final class EntrypointAction -{ - public function __construct( - private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, - private readonly ?ProviderInterface $provider = null, - private readonly ?ProcessorInterface $processor = null, - private readonly array $documentationFormats = [], - ) { - } - - /** - * @return Entrypoint|Response - */ - public function __invoke(?Request $request = null) - { - if ($this->provider && $this->processor) { - $context = ['request' => $request]; - $operation = new Get(outputFormats: $this->documentationFormats, read: true, serialize: true, class: Entrypoint::class, provider: fn () => new Entrypoint($this->resourceNameCollectionFactory->create())); - $body = $this->provider->provide($operation, [], $context); - // see https://github.com/api-platform/core/issues/5845#issuecomment-1732400657 - if ($request && ($apiOperation = $request->attributes->get('_api_operation'))) { - $operation = $apiOperation; - } - - return $this->processor->process($body, $operation, [], $context); - } - - return new Entrypoint($this->resourceNameCollectionFactory->create()); - } -} diff --git a/src/Action/ExceptionAction.php b/src/Action/ExceptionAction.php deleted file mode 100644 index 78e08c7b6cb..00000000000 --- a/src/Action/ExceptionAction.php +++ /dev/null @@ -1,117 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Util\ErrorFormatGuesser; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface as ApiPlatformConstraintViolationListAwareExceptionInterface; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * Renders a normalized exception for a given see [FlattenException](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php). - * - * @author Baptiste Meyer - * @author Kévin Dunglas - * - * @deprecated since API Platform 3 and Error resource is used {@see ApiPlatform\Symfony\EventListener\ErrorListener} - */ -final class ExceptionAction -{ - use OperationRequestInitiatorTrait; - - /** - * @param array $errorFormats A list of enabled error formats - * @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code - */ - public function __construct(private readonly SerializerInterface $serializer, private readonly array $errorFormats, private readonly array $exceptionToStatus = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Converts an exception to a JSON response. - */ - public function __invoke(FlattenException $exception, Request $request): Response - { - $operation = $this->initializeOperation($request); - $exceptionClass = $exception->getClass(); - $statusCode = $exception->getStatusCode(); - - $exceptionToStatus = array_merge( - $this->exceptionToStatus, - $operation ? $operation->getExceptionToStatus() ?? [] : $this->getOperationExceptionToStatus($request) - ); - - foreach ($exceptionToStatus as $class => $status) { - if (is_a($exceptionClass, $class, true)) { - $statusCode = $status; - - break; - } - } - - $headers = $exception->getHeaders(); - $format = ErrorFormatGuesser::guessErrorFormat($request, $this->errorFormats); - $headers['Content-Type'] = \sprintf('%s; charset=utf-8', $format['value'][0]); - $headers['X-Content-Type-Options'] = 'nosniff'; - $headers['X-Frame-Options'] = 'deny'; - - $context = ['statusCode' => $statusCode, 'rfc_7807_compliant_errors' => $operation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false]; - $error = $request->attributes->get('exception') ?? $exception; - if ($error instanceof ConstraintViolationListAwareExceptionInterface || $error instanceof ApiPlatformConstraintViolationListAwareExceptionInterface) { - $error = $error->getConstraintViolationList(); - } elseif (method_exists($error, 'getViolations') && $error->getViolations() instanceof ConstraintViolationListInterface) { - $error = $error->getViolations(); - } else { - $error = $exception; - } - - $serializerFormat = $format['key']; - if ('json' === $serializerFormat && 'application/problem+json' === $format['value'][0]) { - $serializerFormat = 'jsonproblem'; - } - - return new Response($this->serializer->serialize($error, $serializerFormat, $context), $statusCode, $headers); - } - - private function getOperationExceptionToStatus(Request $request): array - { - $attributes = RequestAttributesExtractor::extractAttributes($request); - - if ([] === $attributes) { - return []; - } - - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($attributes['resource_class']); - /** @var HttpOperation $operation */ - $operation = $resourceMetadataCollection->getOperation($attributes['operation_name'] ?? null); - $exceptionToStatus = [$operation->getExceptionToStatus() ?: []]; - - foreach ($resourceMetadataCollection as $resourceMetadata) { - /* @var ApiResource $resourceMetadata */ - $exceptionToStatus[] = $resourceMetadata->getExceptionToStatus() ?: []; - } - - return array_merge(...$exceptionToStatus); - } -} diff --git a/src/Action/NotExposedAction.php b/src/Action/NotExposedAction.php deleted file mode 100644 index 183ad439443..00000000000 --- a/src/Action/NotExposedAction.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use ApiPlatform\Exception\NotExposedHttpException; -use Symfony\Component\HttpFoundation\Request; - -/** - * An action which always returns HTTP 404 Not Found with an explanation for why the operation is not exposed. - * - * @deprecated use ApiPlatform\Symfony\Action\NotExposedAction - */ -final class NotExposedAction -{ - public function __invoke(Request $request): never - { - $message = 'This route does not aim to be called.'; - if ('api_genid' === $request->attributes->get('_route')) { - $message = 'This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.'; - } - - throw new NotExposedHttpException($message); - } -} diff --git a/src/Action/NotFoundAction.php b/src/Action/NotFoundAction.php deleted file mode 100644 index 8005756e426..00000000000 --- a/src/Action/NotFoundAction.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * An action which always returns HTTP 404 Not Found. Useful for disabling an operation. - * - * @deprecated use ApiPlatform\Symfony\Action\NotFoundAction - */ -final class NotFoundAction -{ - public function __invoke(): void - { - throw new NotFoundHttpException(); - } -} diff --git a/src/Action/PlaceholderAction.php b/src/Action/PlaceholderAction.php deleted file mode 100644 index 1a2acd2e6a8..00000000000 --- a/src/Action/PlaceholderAction.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -/** - * Placeholder returning the data passed in parameter. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Symfony\Action\PlaceholderAction - */ -final class PlaceholderAction -{ - /** - * @param object $data - * - * @return object - */ - public function __invoke($data) - { - return $data; - } -} diff --git a/src/Api/CompositeIdentifierParser.php b/src/Api/CompositeIdentifierParser.php deleted file mode 100644 index 97a1fabe2d9..00000000000 --- a/src/Api/CompositeIdentifierParser.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -/** - * Normalizes a composite identifier. - * - * @author Antoine Bluchet - * - * @deprecated - */ -final class CompositeIdentifierParser -{ - public const COMPOSITE_IDENTIFIER_REGEXP = '/(\w+)=(?<=\w=)(.*?)(?=;\w+=)|(\w+)=([^;]*);?$/'; - - private function __construct() - { - } - - /* - * Normalize takes a string and gives back an array of identifiers. - * - * For example: foo=0;bar=2 returns ['foo' => 0, 'bar' => 2]. - */ - public static function parse(string $identifier): array - { - $matches = []; - $identifiers = []; - $num = preg_match_all(self::COMPOSITE_IDENTIFIER_REGEXP, $identifier, $matches, \PREG_SET_ORDER); - - foreach ($matches as $i => $match) { - if ($i === $num - 1) { - $identifiers[$match[3]] = $match[4]; - continue; - } - $identifiers[$match[1]] = $match[2]; - } - - return $identifiers; - } - - /** - * Renders composite identifiers to string using: key=value;key2=value2. - */ - public static function stringify(array $identifiers): string - { - $composite = []; - foreach ($identifiers as $name => $value) { - $composite[] = \sprintf('%s=%s', $name, $value); - } - - return implode(';', $composite); - } -} diff --git a/src/Api/Entrypoint.php b/src/Api/Entrypoint.php deleted file mode 100644 index ec8f0b8ec39..00000000000 --- a/src/Api/Entrypoint.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Resource\ResourceNameCollection; - -/** - * The first path you will see in the API. - * - * @deprecated use ApiPlatform\Documentation\Entrypoint - * - * @author Amrouche Hamza - */ -final class Entrypoint -{ - public function __construct(private readonly ResourceNameCollection $resourceNameCollection) - { - } - - public function getResourceNameCollection(): ResourceNameCollection - { - return $this->resourceNameCollection; - } -} diff --git a/src/Api/FilterInterface.php b/src/Api/FilterInterface.php deleted file mode 100644 index b0d23e57d89..00000000000 --- a/src/Api/FilterInterface.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -if (interface_exists(\ApiPlatform\Metadata\FilterInterface::class)) { - trigger_deprecation('api-platform', '3.3', \sprintf('%s is deprecated in favor of %s. This class will be removed in 4.0.', FilterInterface::class, \ApiPlatform\Metadata\FilterInterface::class)); - class_alias( - \ApiPlatform\Metadata\FilterInterface::class, - __NAMESPACE__.'\FilterInterface' - ); - - if (false) { // @phpstan-ignore-line - interface FilterInterface extends \ApiPlatform\Metadata\FilterInterface - { - } - } -} else { - /** - * Filters applicable on a resource. - * - * @author Kévin Dunglas - * - * @deprecated - */ - interface FilterInterface - { - /** - * Gets the description of this filter for the given resource. - * - * Returns an array with the filter parameter names as keys and array with the following data as values: - * - property: the property where the filter is applied - * - type: the type of the filter - * - required: if this filter is required - * - strategy (optional): the used strategy - * - is_collection (optional): if this filter is for collection - * - swagger (optional): additional parameters for the path operation, - * e.g. 'swagger' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'type' => 'integer', - * ] - * - openapi (optional): additional parameters for the path operation in the version 3 spec, - * e.g. 'openapi' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'schema' => [ - * 'type' => 'integer', - * ] - * ] - * - schema (optional): schema definition, - * e.g. 'schema' => [ - * 'type' => 'string', - * 'enum' => ['value_1', 'value_2'], - * ] - * The description can contain additional data specific to a filter. - * - * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters - */ - public function getDescription(string $resourceClass): array; - } -} diff --git a/src/Api/FilterLocatorTrait.php b/src/Api/FilterLocatorTrait.php deleted file mode 100644 index bc427336c87..00000000000 --- a/src/Api/FilterLocatorTrait.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidArgumentException; -use Psr\Container\ContainerInterface; - -/** - * Manipulates filters with a backward compatibility between the new filter locator and the deprecated filter collection. - * - * @author Baptiste Meyer - * - * @deprecated - * - * @internal - */ -trait FilterLocatorTrait -{ - private ?ContainerInterface $filterLocator = null; - - /** - * Sets a filter locator with a backward compatibility. - */ - private function setFilterLocator(?ContainerInterface $filterLocator, bool $allowNull = false): void - { - if ($filterLocator instanceof ContainerInterface || (null === $filterLocator && $allowNull)) { - $this->filterLocator = $filterLocator; - } else { - throw new InvalidArgumentException(\sprintf('The "$filterLocator" argument is expected to be an implementation of the "%s" interface%s.', ContainerInterface::class, $allowNull ? ' or null' : '')); - } - } - - /** - * Gets a filter with a backward compatibility. - */ - private function getFilter(string $filterId): ?FilterInterface - { - if ($this->filterLocator && $this->filterLocator->has($filterId)) { - return $this->filterLocator->get($filterId); - } - - return null; - } -} diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php deleted file mode 100644 index 0db5d0a74a2..00000000000 --- a/src/Api/IdentifiersExtractor.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; -use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * {@inheritdoc} - * - * @deprecated use ApiPlatform\Metadata\IdentifiersExtractor instead - * - * @author Antoine Bluchet - */ -final class IdentifiersExtractor implements IdentifiersExtractorInterface -{ - use ResourceClassInfoTrait; - private readonly PropertyAccessorInterface $propertyAccessor; - - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ?PropertyAccessorInterface $propertyAccessor = null) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->resourceClassResolver = $resourceClassResolver; - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } - - /** - * {@inheritdoc} - * - * TODO: 3.0 identifiers should be stringable? - */ - public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array - { - if (!$this->isResourceClass($this->getObjectClass($item))) { - return ['id' => $this->propertyAccessor->getValue($item, 'id')]; - } - - if ($operation && $operation->getClass()) { - return $this->getIdentifiersFromOperation($item, $operation, $context); - } - - $resourceClass = $this->getResourceClass($item, true); - $operation ??= $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true); - - return $this->getIdentifiersFromOperation($item, $operation, $context); - } - - private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array - { - if ($operation instanceof HttpOperation) { - $links = $operation->getUriVariables(); - } elseif ($operation instanceof GraphQlOperation) { - $links = $operation->getLinks(); - } - - $identifiers = []; - foreach ($links ?? [] as $k => $link) { - $linkIdentifiers = $link->getIdentifiers() ?? [$k]; - if (1 < \count($linkIdentifiers)) { - $compositeIdentifiers = []; - foreach ($linkIdentifiers as $identifier) { - $compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName()); - } - - $identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers); - continue; - } - - $parameterName = $link->getParameterName(); - $identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $linkIdentifiers[0], $parameterName, $link->getToProperty()); - } - - return $identifiers; - } - - /** - * Gets the value of the given class property. - */ - private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, ?string $toProperty = null): float|bool|int|string - { - if ($item instanceof $class) { - try { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName); - } catch (NoSuchPropertyException $e) { - throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); - } - } - - if ($toProperty) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName); - } - - $resourceClass = $this->getResourceClass($item, true); - foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - - $types = $propertyMetadata->getBuiltinTypes(); - if (null === ($type = $types[0] ?? null)) { - continue; - } - - try { - if ($type->isCollection()) { - $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; - - if (null !== $collectionValueType && $collectionValueType->getClassName() === $class) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, \sprintf('%s[0].%s', $propertyName, $property)), $parameterName); - } - } - - if ($type->getClassName() === $class) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName); - } - } catch (NoSuchPropertyException $e) { - throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); - } - } - - throw new RuntimeException('Not able to retrieve identifiers.'); - } - - /** - * TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior. - * - * @param mixed|\Stringable $identifierValue - */ - private function resolveIdentifierValue(mixed $identifierValue, string $parameterName): float|bool|int|string - { - if (null === $identifierValue) { - throw new RuntimeException('No identifier value found, did you forget to persist the entity?'); - } - - if (\is_scalar($identifierValue)) { - return $identifierValue; - } - - if ($identifierValue instanceof \Stringable) { - return (string) $identifierValue; - } - - if ($identifierValue instanceof \BackedEnum) { - return (string) $identifierValue->value; - } - - throw new RuntimeException(\sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName)); - } -} diff --git a/src/Api/IdentifiersExtractorInterface.php b/src/Api/IdentifiersExtractorInterface.php deleted file mode 100644 index 1ad79b4fd19..00000000000 --- a/src/Api/IdentifiersExtractorInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\Operation; - -/** - * Extracts identifiers for a given Resource according to the retrieved Metadata. - * - * @author Antoine Bluchet - */ -interface IdentifiersExtractorInterface -{ - /** - * Finds identifiers from an Item (object). - * - * @throws RuntimeException - */ - public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array; -} diff --git a/src/Api/IriConverterInterface.php b/src/Api/IriConverterInterface.php deleted file mode 100644 index b2990f339d2..00000000000 --- a/src/Api/IriConverterInterface.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\Operation; - -/** - * Converts item and resources to IRI and vice versa. - * - * @author Kévin Dunglas - */ -interface IriConverterInterface -{ - /** - * Retrieves an item from its IRI. - * - * @throws InvalidArgumentException - * @throws ItemNotFoundException - */ - public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object; - - /** - * Gets the IRI associated with the given item. - * - * @param object|class-string $resource - * - * @throws InvalidArgumentException - * @throws RuntimeException - */ - public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string; -} diff --git a/src/Api/QueryParameterValidator/QueryParameterValidator.php b/src/Api/QueryParameterValidator/QueryParameterValidator.php deleted file mode 100644 index 4c3954865bd..00000000000 --- a/src/Api/QueryParameterValidator/QueryParameterValidator.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator; - -use ApiPlatform\ParameterValidator\ParameterValidator as NewQueryParameterValidator; - -/** - * Validates query parameters depending on filter description. - * - * @deprecated use ApiPlatform\QueryParameterValidator\QueryParameterValidator instead - */ -class QueryParameterValidator extends NewQueryParameterValidator -{ -} diff --git a/src/Api/QueryParameterValidator/Validator/ArrayItems.php b/src/Api/QueryParameterValidator/Validator/ArrayItems.php deleted file mode 100644 index 940d5f77e66..00000000000 --- a/src/Api/QueryParameterValidator/Validator/ArrayItems.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\ArrayItems instead - */ -final class ArrayItems implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - if (!\array_key_exists($name, $queryParameters)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxItems = $filterDescription['openapi']['maxItems'] ?? $filterDescription['swagger']['maxItems'] ?? null; - $minItems = $filterDescription['openapi']['minItems'] ?? $filterDescription['swagger']['minItems'] ?? null; - $uniqueItems = $filterDescription['openapi']['uniqueItems'] ?? $filterDescription['swagger']['uniqueItems'] ?? false; - - $errorList = []; - - $value = $this->getValue($name, $filterDescription, $queryParameters); - $nbItems = \count($value); - - if (null !== $maxItems && $nbItems > $maxItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain less than %d values', $name, $maxItems); - } - - if (null !== $minItems && $nbItems < $minItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain more than %d values', $name, $minItems); - } - - if (true === $uniqueItems && $nbItems > \count(array_unique($value))) { - $errorList[] = \sprintf('Query parameter "%s" must contain unique values', $name); - } - - return $errorList; - } - - private function getValue(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - - if (empty($value) && '0' !== $value) { - return []; - } - - if (\is_array($value)) { - return $value; - } - - $collectionFormat = $filterDescription['openapi']['collectionFormat'] ?? $filterDescription['swagger']['collectionFormat'] ?? 'csv'; - - return explode(self::getSeparator($collectionFormat), (string) $value) ?: []; // @phpstan-ignore-line - } - - /** - * @return non-empty-string - */ - private static function getSeparator(string $collectionFormat): string - { - return match ($collectionFormat) { - 'csv' => ',', - 'ssv' => ' ', - 'tsv' => '\t', - 'pipes' => '|', - default => throw new \InvalidArgumentException(\sprintf('Unknown collection format %s', $collectionFormat)), - }; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Bounds.php b/src/Api/QueryParameterValidator/Validator/Bounds.php deleted file mode 100644 index 0983ccc5d6e..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Bounds.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Bounds instead - */ -final class Bounds implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maximum = $filterDescription['openapi']['maximum'] ?? $filterDescription['swagger']['maximum'] ?? null; - $minimum = $filterDescription['openapi']['minimum'] ?? $filterDescription['swagger']['minimum'] ?? null; - - $errorList = []; - - if (null !== $maximum) { - if (($filterDescription['openapi']['exclusiveMaximum'] ?? $filterDescription['swagger']['exclusiveMaximum'] ?? false) && $value >= $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than %s', $name, $maximum); - } elseif ($value > $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than or equal to %s', $name, $maximum); - } - } - - if (null !== $minimum) { - if (($filterDescription['openapi']['exclusiveMinimum'] ?? $filterDescription['swagger']['exclusiveMinimum'] ?? false) && $value <= $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than %s', $name, $minimum); - } elseif ($value < $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than or equal to %s', $name, $minimum); - } - } - - return $errorList; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Enum.php b/src/Api/QueryParameterValidator/Validator/Enum.php deleted file mode 100644 index 53a1c2c1b9a..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Enum.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Enum instead - */ -final class Enum implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $enum = $filterDescription['openapi']['enum'] ?? $filterDescription['swagger']['enum'] ?? null; - - if (null !== $enum && !\in_array($value, $enum, true)) { - return [ - \sprintf('Query parameter "%s" must be one of "%s"', $name, implode(', ', $enum)), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Length.php b/src/Api/QueryParameterValidator/Validator/Length.php deleted file mode 100644 index 9286cdb1975..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Length.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Length instead - */ -final class Length implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxLength = $filterDescription['openapi']['maxLength'] ?? $filterDescription['swagger']['maxLength'] ?? null; - $minLength = $filterDescription['openapi']['minLength'] ?? $filterDescription['swagger']['minLength'] ?? null; - - $errorList = []; - - if (null !== $maxLength && mb_strlen($value) > $maxLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be lower than or equal to %s', $name, $maxLength); - } - - if (null !== $minLength && mb_strlen($value) < $minLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be greater than or equal to %s', $name, $minLength); - } - - return $errorList; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/MultipleOf.php b/src/Api/QueryParameterValidator/Validator/MultipleOf.php deleted file mode 100644 index 77e48ef17af..00000000000 --- a/src/Api/QueryParameterValidator/Validator/MultipleOf.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\MultipleOf instead - */ -final class MultipleOf implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $multipleOf = $filterDescription['openapi']['multipleOf'] ?? $filterDescription['swagger']['multipleOf'] ?? null; - - if (null !== $multipleOf && 0 !== ($value % $multipleOf)) { - return [ - \sprintf('Query parameter "%s" must multiple of %s', $name, $multipleOf), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Pattern.php b/src/Api/QueryParameterValidator/Validator/Pattern.php deleted file mode 100644 index 8d134761042..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Pattern.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Pattern instead - */ -final class Pattern implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $pattern = $filterDescription['openapi']['pattern'] ?? $filterDescription['swagger']['pattern'] ?? null; - - if (null !== $pattern && !preg_match($pattern, $value)) { - return [ - \sprintf('Query parameter "%s" must match pattern %s', $name, $pattern), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Required.php b/src/Api/QueryParameterValidator/Validator/Required.php deleted file mode 100644 index bc8c02fffd1..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Required.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; -use ApiPlatform\State\Util\RequestParser; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Required instead - */ -final class Required implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - // filter is not required, the `checkRequired` method can not break - if (!($filterDescription['required'] ?? false)) { - return []; - } - - // if query param is not given, then break - if (!$this->requestHasQueryParameter($queryParameters, $name)) { - return [ - \sprintf('Query parameter "%s" is required', $name), - ]; - } - - $this->checkFilterDeprecations($filterDescription); - - // if query param is empty and the configuration does not allow it - if (!($filterDescription['openapi']['allowEmptyValue'] ?? $filterDescription['swagger']['allowEmptyValue'] ?? false) && empty($this->requestGetQueryParameter($queryParameters, $name))) { - return [ - \sprintf('Query parameter "%s" does not allow empty value', $name), - ]; - } - - return []; - } - - /** - * Test if request has required parameter. - */ - private function requestHasQueryParameter(array $queryParameters, string $name): bool - { - $matches = RequestParser::parseRequestParams($name); - if (!$matches) { - return false; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return false; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - return \is_array($queryParameter) && isset($queryParameter[$keyName]); - } - - return \array_key_exists((string) $rootName, $queryParameters); - } - - /** - * Test if required filter is valid. It validates array notation too like "required[bar]". - */ - private function requestGetQueryParameter(array $queryParameters, string $name) - { - $matches = RequestParser::parseRequestParams($name); - if (empty($matches)) { - return null; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return null; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - if (\is_array($queryParameter) && isset($queryParameter[$keyName])) { - return $queryParameter[$keyName]; - } - - return null; - } - - return $queryParameters[(string) $rootName]; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php b/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php deleted file mode 100644 index 2bb50512048..00000000000 --- a/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator as ParameterValidatorComponent; - -/** @deprecated use \ApiPlatform\ParameterValidator\Validator\ValidatorInterface instead */ -interface ValidatorInterface extends ParameterValidatorComponent\ValidatorInterface -{ -} diff --git a/src/Api/ResourceClassResolver.php b/src/Api/ResourceClassResolver.php deleted file mode 100644 index 03b8781e67a..00000000000 --- a/src/Api/ResourceClassResolver.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; - -/** - * {@inheritdoc} - * - * @deprecated replaced by ApiPlatform\Metadata\ResourceClassResolver - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class ResourceClassResolver implements ResourceClassResolverInterface -{ - use ClassInfoTrait; - private array $localIsResourceClassCache = []; - private array $localMostSpecificResourceClassCache = []; - - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory) - { - } - - /** - * {@inheritdoc} - */ - public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string - { - if ($strict && null === $resourceClass) { - throw new InvalidArgumentException('Strict checking is only possible when resource class is specified.'); - } - - $objectClass = \is_object($value) ? $this->getObjectClass($value) : null; - $actualClass = ($objectClass && (!$value instanceof \Traversable || $this->isResourceClass($objectClass))) ? $this->getObjectClass($value) : null; - - if (null === $actualClass && null === $resourceClass) { - throw new InvalidArgumentException('Resource type could not be determined. Resource class must be specified.'); - } - - if (null !== $actualClass && !$this->isResourceClass($actualClass)) { - throw new InvalidArgumentException(\sprintf('No resource class found for object of type "%s".', $actualClass)); - } - - if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) { - throw new InvalidArgumentException(\sprintf('Specified class "%s" is not a resource class.', $resourceClass)); - } - - if ($strict && null !== $actualClass && !is_a($actualClass, $resourceClass, true)) { - throw new InvalidArgumentException(\sprintf('Object of type "%s" does not match "%s" resource class.', $actualClass, $resourceClass)); - } - - $targetClass = $actualClass ?? $resourceClass; - - if (isset($this->localMostSpecificResourceClassCache[$targetClass])) { - return $this->localMostSpecificResourceClassCache[$targetClass]; - } - - $mostSpecificResourceClass = null; - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClassName) { - if (!is_a($targetClass, $resourceClassName, true)) { - continue; - } - - if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass)) { - $mostSpecificResourceClass = $resourceClassName; - } - } - - if (null === $mostSpecificResourceClass) { - throw new \LogicException('Unexpected execution flow.'); - } - - $this->localMostSpecificResourceClassCache[$targetClass] = $mostSpecificResourceClass; - - return $mostSpecificResourceClass; - } - - /** - * {@inheritdoc} - */ - public function isResourceClass(string $type): bool - { - if (isset($this->localIsResourceClassCache[$type])) { - return $this->localIsResourceClassCache[$type]; - } - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - if (is_a($type, $resourceClass, true)) { - return $this->localIsResourceClassCache[$type] = true; - } - } - - return $this->localIsResourceClassCache[$type] = false; - } -} diff --git a/src/Api/ResourceClassResolverInterface.php b/src/Api/ResourceClassResolverInterface.php deleted file mode 100644 index 75e6a021d64..00000000000 --- a/src/Api/ResourceClassResolverInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidArgumentException; - -/** - * Guesses which resource is associated with a given object. - * - * @author Kévin Dunglas - */ -interface ResourceClassResolverInterface -{ - /** - * Guesses the associated resource. - * - * @param string $resourceClass The expected resource class - * @param bool $strict If true, value must match the expected resource class - * - * @throws InvalidArgumentException - */ - public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string; - - /** - * Is the given class a resource class? - */ - public function isResourceClass(string $type): bool; -} diff --git a/src/Api/UriVariableTransformer/DateTimeUriVariableTransformer.php b/src/Api/UriVariableTransformer/DateTimeUriVariableTransformer.php deleted file mode 100644 index f657fc5cda8..00000000000 --- a/src/Api/UriVariableTransformer/DateTimeUriVariableTransformer.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\UriVariableTransformer; - -use ApiPlatform\Api\UriVariableTransformerInterface; -use ApiPlatform\Exception\InvalidUriVariableException; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; -use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; - -final class DateTimeUriVariableTransformer implements UriVariableTransformerInterface -{ - private readonly DateTimeNormalizer $dateTimeNormalizer; - - public function __construct() - { - $this->dateTimeNormalizer = new DateTimeNormalizer(); - } - - public function transform(mixed $value, array $types, array $context = []): \DateTimeInterface - { - try { - return $this->dateTimeNormalizer->denormalize($value, $types[0], null, $context); - } catch (NotNormalizableValueException $e) { - throw new InvalidUriVariableException($e->getMessage(), $e->getCode(), $e); - } - } - - public function supportsTransformation(mixed $value, array $types, array $context = []): bool - { - return $this->dateTimeNormalizer->supportsDenormalization($value, $types[0]); - } -} diff --git a/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php b/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php deleted file mode 100644 index 9c649427a50..00000000000 --- a/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\UriVariableTransformer; - -use ApiPlatform\Api\UriVariableTransformerInterface; -use Symfony\Component\PropertyInfo\Type; - -final class IntegerUriVariableTransformer implements UriVariableTransformerInterface -{ - public function transform(mixed $value, array $types, array $context = []): int - { - return (int) $value; - } - - public function supportsTransformation(mixed $value, array $types, array $context = []): bool - { - return Type::BUILTIN_TYPE_INT === $types[0] && \is_string($value); - } -} diff --git a/src/Api/UriVariableTransformerInterface.php b/src/Api/UriVariableTransformerInterface.php deleted file mode 100644 index c3a0013244e..00000000000 --- a/src/Api/UriVariableTransformerInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidUriVariableException; - -interface UriVariableTransformerInterface -{ - /** - * Transforms the value of a URI variable (identifier) to its type. - * - * @param mixed $value The URI variable value to transform - * @param array $types The guessed type behind the URI variable - * @param array $context Options available to the transformer - * - * @throws InvalidUriVariableException Occurs when the URI variable could not be transformed - */ - public function transform(mixed $value, array $types, array $context = []); - - /** - * Checks whether the value of a URI variable can be transformed to its type by this transformer. - * - * @param mixed $value The URI variable value to transform - * @param array $types The types to which the URI variable value should be transformed - * @param array $context Options available to the transformer - */ - public function supportsTransformation(mixed $value, array $types, array $context = []): bool; -} diff --git a/src/Api/UriVariablesConverter.php b/src/Api/UriVariablesConverter.php deleted file mode 100644 index 59999547cdc..00000000000 --- a/src/Api/UriVariablesConverter.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidUriVariableException; -use ApiPlatform\Metadata\Link; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use Symfony\Component\PropertyInfo\Type; - -/** - * UriVariables converter that chains uri variables transformers. - * - * @author Antoine Bluchet - */ -final class UriVariablesConverter implements UriVariablesConverterInterface -{ - /** - * @param iterable $uriVariableTransformers - */ - public function __construct(private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly iterable $uriVariableTransformers) - { - } - - /** - * {@inheritdoc} - * - * To handle the composite identifiers type correctly, use an `uri_variables_map` that maps uriVariables to their uriVariablesDefinition. - * Indeed, a composite identifier will already be parsed, and their corresponding properties will be the parameterName and not the defined - * identifiers. - */ - public function convert(array $uriVariables, string $class, array $context = []): array - { - $operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation(); - $context += ['operation' => $operation]; - $uriVariablesDefinitions = $operation->getUriVariables() ?? []; - - foreach ($uriVariables as $parameterName => $value) { - $uriVariableDefinition = $context['uri_variables_map'][$parameterName] ?? $uriVariablesDefinitions[$parameterName] ?? $uriVariablesDefinitions['id'] ?? new Link(); - - // When a composite identifier is used, we assume that the parameterName is the property to find our type - $properties = $uriVariableDefinition->getIdentifiers() ?? [$parameterName]; - if ($uriVariableDefinition->getCompositeIdentifier()) { - $properties = [$parameterName]; - } - - if (!$types = $this->getIdentifierTypes($uriVariableDefinition->getFromClass() ?? $class, $properties)) { - continue; - } - - foreach ($this->uriVariableTransformers as $uriVariableTransformer) { - if (!$uriVariableTransformer->supportsTransformation($value, $types, $context)) { - continue; - } - - try { - $uriVariables[$parameterName] = $uriVariableTransformer->transform($value, $types, $context); - break; - } catch (InvalidUriVariableException $e) { - throw new InvalidUriVariableException(\sprintf('Identifier "%s" could not be transformed.', $parameterName), $e->getCode(), $e); - } - } - } - - return $uriVariables; - } - - private function getIdentifierTypes(string $resourceClass, array $properties): array - { - $types = []; - foreach ($properties as $property) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); - foreach ($propertyMetadata->getBuiltinTypes() as $type) { - $types[] = Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType; - } - } - - return $types; - } -} diff --git a/src/Api/UriVariablesConverterInterface.php b/src/Api/UriVariablesConverterInterface.php deleted file mode 100644 index 67c49092221..00000000000 --- a/src/Api/UriVariablesConverterInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidIdentifierException; - -/** - * Identifier converter. - * - * @author Antoine Bluchet - */ -interface UriVariablesConverterInterface -{ - /** - * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. - * - * @param array $data URI variables to convert to PHP values - * @param string $class The class to which the URI variables belong to - * - * @throws InvalidIdentifierException - * - * @return array Array indexed by identifiers properties with their values denormalized - */ - public function convert(array $data, string $class, array $context = []): array; -} diff --git a/src/Api/UrlGeneratorInterface.php b/src/Api/UrlGeneratorInterface.php deleted file mode 100644 index 1a62b4fdf3f..00000000000 --- a/src/Api/UrlGeneratorInterface.php +++ /dev/null @@ -1,86 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use Symfony\Component\Routing\Exception\InvalidParameterException; -use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; -use Symfony\Component\Routing\Exception\RouteNotFoundException; - -/** - * UrlGeneratorInterface is the interface that all URL generator classes must implement. - * - * This interface has been imported and adapted from the Symfony project. - * - * The constants in this interface define the different types of resource references that - * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 - * We are using the term "URL" instead of "URI" as this is more common in web applications - * and we do not need to distinguish them as the difference is mostly semantical and - * less technical. Generating URIs, i.e. representation-independent resource identifiers, - * is also possible. - * - * @author Fabien Potencier - * @author Tobias Schultze - * @copyright Fabien Potencier - * - * @deprecated moved to ApiPlatform\Metadata\UrlGeneratorInterface - */ -interface UrlGeneratorInterface -{ - /** - * Generates an absolute URL, e.g. "http://example.com/dir/file". - */ - public const ABS_URL = 0; - - /** - * Generates an absolute path, e.g. "/dir/file". - */ - public const ABS_PATH = 1; - - /** - * Generates a relative path based on the current request path, e.g. "../parent-file". - * - * @see UrlGenerator::getRelativePath() - */ - public const REL_PATH = 2; - - /** - * Generates a network path, e.g. "//example.com/dir/file". - * Such reference reuses the current scheme but specifies the host. - */ - public const NET_PATH = 3; - - /** - * Generates a URL or path for a specific route based on the given parameters. - * - * Parameters that reference placeholders in the route pattern will substitute them in the - * path or host. Extra params are added as query string to the URL. - * - * When the passed reference type cannot be generated for the route because it requires a different - * host or scheme than the current one, the method will return a more comprehensive reference - * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH - * but the route requires the https scheme whereas the current scheme is http, it will instead return an - * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches - * the route in any case. - * - * If there is no route with the given name, the generator must throw the RouteNotFoundException. - * - * The special parameter _fragment will be used as the document fragment suffixed to the final URL. - * - * @throws RouteNotFoundException If the named route doesn't exist - * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route - * @throws InvalidParameterException When a parameter value for a placeholder is not correct because - * it does not match the requirement - */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; -} diff --git a/src/Doctrine/Common/Filter/SearchFilterTrait.php b/src/Doctrine/Common/Filter/SearchFilterTrait.php index d04fe78ba19..bf64d45370d 100644 --- a/src/Doctrine/Common/Filter/SearchFilterTrait.php +++ b/src/Doctrine/Common/Filter/SearchFilterTrait.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Common\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IdentifiersExtractorInterface; @@ -32,9 +30,9 @@ trait SearchFilterTrait { use PropertyHelperTrait; - protected IriConverterInterface|LegacyIriConverterInterface $iriConverter; + protected IriConverterInterface $iriConverter; protected PropertyAccessorInterface $propertyAccessor; - protected IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null; + protected ?IdentifiersExtractorInterface $identifiersExtractor = null; /** * {@inheritdoc} @@ -111,7 +109,7 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; - abstract protected function getIriConverter(): LegacyIriConverterInterface|IriConverterInterface; + abstract protected function getIriConverter(): IriConverterInterface; abstract protected function getPropertyAccessor(): PropertyAccessorInterface; diff --git a/src/Doctrine/Common/State/PersistProcessor.php b/src/Doctrine/Common/State/PersistProcessor.php index 20e93ac4399..abc21bf9e15 100644 --- a/src/Doctrine/Common/State/PersistProcessor.php +++ b/src/Doctrine/Common/State/PersistProcessor.php @@ -48,7 +48,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = // PUT: reset the existing object managed by Doctrine and merge data sent by the user in it // This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported: // https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555 - if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) { + if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true)) { \assert(method_exists($manager, 'getReference')); $newData = $data; $identifiers = array_reverse($uriVariables); diff --git a/src/Doctrine/Common/Tests/CollectionPaginatorTest.php b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php index 474dd6b4c5b..415ee02c536 100644 --- a/src/Doctrine/Common/Tests/CollectionPaginatorTest.php +++ b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php @@ -19,9 +19,7 @@ class CollectionPaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/SelectablePaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php index 9e1d37adc7c..7f9c8a37b3d 100644 --- a/src/Doctrine/Common/Tests/SelectablePaginatorTest.php +++ b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php @@ -19,9 +19,7 @@ class SelectablePaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php index a71cb3ddb72..b16a9e99f81 100644 --- a/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php +++ b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php @@ -19,9 +19,7 @@ class SelectablePartialPaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php index 1793614040a..f510a661e7e 100644 --- a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php +++ b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php @@ -91,9 +91,7 @@ public static function getTrackingPolicyParameters(): array ]; } - /** - * @dataProvider getTrackingPolicyParameters - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getTrackingPolicyParameters')] public function testTrackingPolicy(string $metadataClass, bool $deferredExplicit, bool $persisted): void { $dummy = new Dummy(); diff --git a/src/Doctrine/Common/composer.json b/src/Doctrine/Common/composer.json index 6abfecea8fc..8ecfb1da2a5 100644 --- a/src/Doctrine/Common/composer.json +++ b/src/Doctrine/Common/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", "doctrine/collections": "^2.1", @@ -33,16 +33,14 @@ "require-dev": { "doctrine/mongodb-odm": "^2.6", "doctrine/orm": "^2.17 || ^3.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2" }, "conflict": { "doctrine/persistence": "<1.3" }, "suggest": { "phpstan/phpdoc-parser": "For PHP documentation support.", - "symfony/messenger": "For async mercure updates.", "symfony/yaml": "For YAML resource configuration.", "symfony/config": "For XML resource configuration.", "api-platform/http-cache": "For HTTP cache invalidation.", @@ -66,7 +64,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Doctrine/EventListener/PublishMercureUpdatesListener.php deleted file mode 100644 index 2d58fc153e7..00000000000 --- a/src/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ /dev/null @@ -1,313 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Doctrine\EventListener; - -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface as GraphQlMercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface as GraphQlSubscriptionManagerInterface; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Exception\OperationNotFoundException; -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; -use ApiPlatform\Symfony\Messenger\DispatchTrait; -use Doctrine\Common\EventArgs; -use Doctrine\ODM\MongoDB\Event\OnFlushEventArgs as MongoDbOdmOnFlushEventArgs; -use Doctrine\ORM\Event\OnFlushEventArgs as OrmOnFlushEventArgs; -use Symfony\Component\ExpressionLanguage\ExpressionFunction; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\Mercure\HubRegistry; -use Symfony\Component\Mercure\Update; -use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * Publishes resources updates to the Mercure hub. - * - * @author Kévin Dunglas - * - * @deprecated moved to \ApiPlatform\Doctrine\Common\EventListener\PublishMercureUpdatesListener - */ -final class PublishMercureUpdatesListener -{ - use DispatchTrait; - use ResourceClassInfoTrait; - private const ALLOWED_KEYS = [ - 'topics' => true, - 'data' => true, - 'private' => true, - 'id' => true, - 'type' => true, - 'retry' => true, - 'normalization_context' => true, - 'hub' => true, - 'enable_async_update' => true, - ]; - private readonly ?ExpressionLanguage $expressionLanguage; - private \SplObjectStorage $createdObjects; - private \SplObjectStorage $updatedObjects; - private \SplObjectStorage $deletedObjects; - - /** - * @param array $formats - */ - public function __construct(LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, private readonly LegacyIriConverterInterface|IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly SerializerInterface $serializer, private readonly array $formats, ?MessageBusInterface $messageBus = null, private readonly ?HubRegistry $hubRegistry = null, private readonly ?GraphQlSubscriptionManagerInterface $graphQlSubscriptionManager = null, private readonly ?GraphQlMercureSubscriptionIriGeneratorInterface $graphQlMercureSubscriptionIriGenerator = null, ?ExpressionLanguage $expressionLanguage = null, private bool $includeType = false) - { - if (null === $messageBus && null === $hubRegistry) { - throw new InvalidArgumentException('A message bus or a hub registry must be provided.'); - } - - $this->resourceClassResolver = $resourceClassResolver; - - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->messageBus = $messageBus; - $this->expressionLanguage = $expressionLanguage ?? (class_exists(ExpressionLanguage::class) ? new ExpressionLanguage() : null); - $this->reset(); - - if ($this->expressionLanguage) { - $rawurlencode = ExpressionFunction::fromPhp('rawurlencode', 'escape'); - $this->expressionLanguage->addFunction($rawurlencode); - - $this->expressionLanguage->addFunction( - new ExpressionFunction('get_operation', static fn (string $apiResource, string $name): string => \sprintf('getOperation(%s, %s)', $apiResource, $name), static fn (array $arguments, $apiResource, string $name): Operation => $resourceMetadataFactory->create($resourceClassResolver->getResourceClass($apiResource))->getOperation($name)) - ); - $this->expressionLanguage->addFunction( - new ExpressionFunction('iri', static fn (string $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL, ?string $operation = null): string => \sprintf('iri(%s, %d, %s)', $apiResource, $referenceType, $operation), static fn (array $arguments, $apiResource, int $referenceType = UrlGeneratorInterface::ABS_URL, $operation = null): string => $iriConverter->getIriFromResource($apiResource, $referenceType, $operation)) - ); - } - - if (false === $this->includeType) { - trigger_deprecation('api-platform/core', '3.1', 'Having mercure.include_type (always include @type in Mercure updates, even delete ones) set to false in the configuration is deprecated. It will be true by default in API Platform 4.0.'); - } - } - - /** - * Collects created, updated and deleted objects. - */ - public function onFlush(EventArgs $eventArgs): void - { - if ($eventArgs instanceof OrmOnFlushEventArgs) { - // @phpstan-ignore-next-line - $uow = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager()->getUnitOfWork() : $eventArgs->getEntityManager()->getUnitOfWork(); - } elseif ($eventArgs instanceof MongoDbOdmOnFlushEventArgs) { - $uow = $eventArgs->getDocumentManager()->getUnitOfWork(); - } else { - return; - } - - $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityInsertions' : 'getScheduledDocumentInsertions'; - foreach ($uow->{$methodName}() as $object) { - $this->storeObjectToPublish($object, 'createdObjects'); - } - - $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityUpdates' : 'getScheduledDocumentUpdates'; - foreach ($uow->{$methodName}() as $object) { - $this->storeObjectToPublish($object, 'updatedObjects'); - } - - $methodName = $eventArgs instanceof OrmOnFlushEventArgs ? 'getScheduledEntityDeletions' : 'getScheduledDocumentDeletions'; - foreach ($uow->{$methodName}() as $object) { - $this->storeObjectToPublish($object, 'deletedObjects'); - } - } - - /** - * Publishes updates for changes collected on flush, and resets the store. - */ - public function postFlush(): void - { - try { - foreach ($this->createdObjects as $object) { - $this->publishUpdate($object, $this->createdObjects[$object], 'create'); - } - - foreach ($this->updatedObjects as $object) { - $this->publishUpdate($object, $this->updatedObjects[$object], 'update'); - } - - foreach ($this->deletedObjects as $object) { - $this->publishUpdate($object, $this->deletedObjects[$object], 'delete'); - } - } finally { - $this->reset(); - } - } - - private function reset(): void - { - $this->createdObjects = new \SplObjectStorage(); - $this->updatedObjects = new \SplObjectStorage(); - $this->deletedObjects = new \SplObjectStorage(); - } - - private function storeObjectToPublish(object $object, string $property): void - { - if (null === $resourceClass = $this->getResourceClass($object)) { - return; - } - - $operation = $this->resourceMetadataFactory->create($resourceClass)->getOperation(); - try { - $options = $operation->getMercure() ?? false; - } catch (OperationNotFoundException) { - return; - } - - if (\is_string($options)) { - if (null === $this->expressionLanguage) { - throw new RuntimeException('The Expression Language component is not installed. Try running "composer require symfony/expression-language".'); - } - - $options = $this->expressionLanguage->evaluate($options, ['object' => $object]); - } - - if (false === $options) { - return; - } - - if (true === $options) { - $options = []; - } - - if (!\is_array($options)) { - throw new InvalidArgumentException(\sprintf('The value of the "mercure" attribute of the "%s" resource class must be a boolean, an array of options or an expression returning this array, "%s" given.', $resourceClass, \gettype($options))); - } - - foreach ($options as $key => $value) { - if (!isset(self::ALLOWED_KEYS[$key])) { - throw new InvalidArgumentException(\sprintf('The option "%s" set in the "mercure" attribute of the "%s" resource does not exist. Existing options: "%s"', $key, $resourceClass, implode('", "', self::ALLOWED_KEYS))); - } - } - - $options['enable_async_update'] ??= true; - - if ('deletedObjects' === $property) { - $types = $operation instanceof HttpOperation ? $operation->getTypes() : null; - if (null === $types) { - $types = [$operation->getShortName()]; - } - - // We need to evaluate it here, because in publishUpdate() the resource would be already deleted - $this->evaluateTopics($options, $object); - - $this->deletedObjects[(object) [ - 'id' => $this->iriConverter->getIriFromResource($object), - 'iri' => $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL), - 'type' => 1 === \count($types) ? $types[0] : $types, - ]] = $options; - - return; - } - - $this->{$property}[$object] = $options; - } - - private function publishUpdate(object $object, array $options, string $type): void - { - if ($object instanceof \stdClass) { - // By convention, if the object has been deleted, we send only its IRI and its type. - // This may change in the feature, because it's not JSON Merge Patch compliant, - // and I'm not a fond of this approach. - $iri = $options['topics'] ?? $object->iri; - /** @var string $data */ - $data = json_encode(['@id' => $object->id] + ($this->includeType ? ['@type' => $object->type] : []), \JSON_THROW_ON_ERROR); - } else { - $resourceClass = $this->getObjectClass($object); - $context = $options['normalization_context'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation()->getNormalizationContext() ?? []; - - // We need to evaluate it here, because in storeObjectToPublish() the resource would not have been persisted yet - $this->evaluateTopics($options, $object); - - $iri = $options['topics'] ?? $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_URL); - $data = $options['data'] ?? $this->serializer->serialize($object, key($this->formats), $context); - } - - $updates = array_merge([$this->buildUpdate($iri, $data, $options)], $this->getGraphQlSubscriptionUpdates($object, $options, $type)); - - foreach ($updates as $update) { - if ($options['enable_async_update'] && $this->messageBus) { - $this->dispatch($update); - continue; - } - - $this->hubRegistry->getHub($options['hub'] ?? null)->publish($update); - } - } - - private function evaluateTopics(array &$options, object $object): void - { - if (!($options['topics'] ?? false)) { - return; - } - - $topics = []; - foreach ((array) $options['topics'] as $topic) { - if (!\is_string($topic)) { - $topics[] = $topic; - continue; - } - - if (!str_starts_with($topic, '@=')) { - $topics[] = $topic; - continue; - } - - if (null === $this->expressionLanguage) { - throw new \LogicException('The "@=" expression syntax cannot be used without the Expression Language component. Try running "composer require symfony/expression-language".'); - } - - $topics[] = $this->expressionLanguage->evaluate(substr($topic, 2), ['object' => $object]); - } - - $options['topics'] = $topics; - } - - /** - * @return Update[] - */ - private function getGraphQlSubscriptionUpdates(object $object, array $options, string $type): array - { - if ('update' !== $type || !$this->graphQlSubscriptionManager || !$this->graphQlMercureSubscriptionIriGenerator) { - return []; - } - - $payloads = $this->graphQlSubscriptionManager->getPushPayloads($object); - - $updates = []; - foreach ($payloads as [$subscriptionId, $data]) { - $updates[] = $this->buildUpdate( - $this->graphQlMercureSubscriptionIriGenerator->generateTopicIri($subscriptionId), - (string) (new JsonResponse($data))->getContent(), - $options - ); - } - - return $updates; - } - - /** - * @param string|string[] $iri - */ - private function buildUpdate(string|array $iri, string $data, array $options): Update - { - return new Update($iri, $data, $options['private'] ?? false, $options['id'] ?? null, $options['type'] ?? null, $options['retry'] ?? null); - } -} diff --git a/src/Doctrine/EventListener/PurgeHttpCacheListener.php b/src/Doctrine/EventListener/PurgeHttpCacheListener.php deleted file mode 100644 index 4617ebcc4ad..00000000000 --- a/src/Doctrine/EventListener/PurgeHttpCacheListener.php +++ /dev/null @@ -1,184 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Doctrine\EventListener; - -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Exception\OperationNotFoundException; -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; -use Doctrine\ORM\Mapping\AssociationMapping; -use Doctrine\ORM\PersistentCollection; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * Purges responses containing modified entities from the proxy cache. - * - * @author Kévin Dunglas - * - * @deprecated moved to \ApiPlatform\Doctrine\Common\EventListener\PurgeHttpCacheListener - */ -final class PurgeHttpCacheListener -{ - use ClassInfoTrait; - private readonly PropertyAccessorInterface $propertyAccessor; - private array $tags = []; - - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null) - { - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } - - /** - * Collects tags from the previous and the current version of the updated entities to purge related documents. - */ - public function preUpdate(PreUpdateEventArgs $eventArgs): void - { - $object = $eventArgs->getObject(); - $this->gatherResourceAndItemTags($object, true); - - $changeSet = $eventArgs->getEntityChangeSet(); - // @phpstan-ignore-next-line - $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); - $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings(); - - foreach ($changeSet as $key => $value) { - if (!isset($associationMappings[$key])) { - continue; - } - - $this->addTagsFor($value[0]); - $this->addTagsFor($value[1]); - } - } - - /** - * Collects tags from inserted and deleted entities, including relations. - */ - public function onFlush(OnFlushEventArgs $eventArgs): void - { - // @phpstan-ignore-next-line - $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); - $uow = $em->getUnitOfWork(); - - foreach ($uow->getScheduledEntityInsertions() as $entity) { - $this->gatherResourceAndItemTags($entity, false); - $this->gatherRelationTags($em, $entity); - } - - foreach ($uow->getScheduledEntityUpdates() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); - } - - foreach ($uow->getScheduledEntityDeletions() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); - } - } - - /** - * Purges tags collected during this request, and clears the tag list. - */ - public function postFlush(): void - { - if (empty($this->tags)) { - return; - } - - $this->purger->purge(array_values($this->tags)); - - $this->tags = []; - } - - private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void - { - try { - $resourceClass = $this->resourceClassResolver->getResourceClass($entity); - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection()); - $this->tags[$iri] = $iri; - - if ($purgeItem) { - $this->addTagForItem($entity); - } - } catch (OperationNotFoundException|InvalidArgumentException) { - } - } - - private function gatherRelationTags(EntityManagerInterface $em, object $entity): void - { - $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings(); - /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */ - foreach ($associationMappings as $property => $associationMapping) { - if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { - return; - } - - if ( - \is_array($associationMapping) - && \array_key_exists('targetEntity', $associationMapping) - && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) { - return; - } - - if ($this->propertyAccessor->isReadable($entity, $property)) { - $this->addTagsFor($this->propertyAccessor->getValue($entity, $property)); - } - } - } - - private function addTagsFor(mixed $value): void - { - if (!$value || \is_scalar($value)) { - return; - } - - if (!is_iterable($value)) { - $this->addTagForItem($value); - - return; - } - - if ($value instanceof PersistentCollection) { - $value = clone $value; - } - - foreach ($value as $v) { - $this->addTagForItem($v); - } - } - - private function addTagForItem(mixed $value): void - { - if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { - return; - } - - try { - $iri = $this->iriConverter->getIriFromResource($value); - $this->tags[$iri] = $iri; - } catch (RuntimeException|InvalidArgumentException) { - } - } -} diff --git a/src/Doctrine/Odm/Filter/SearchFilter.php b/src/Doctrine/Odm/Filter/SearchFilter.php index fdb8c9d271c..c370e57dcf0 100644 --- a/src/Doctrine/Odm/Filter/SearchFilter.php +++ b/src/Doctrine/Odm/Filter/SearchFilter.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Odm\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait; use ApiPlatform\Metadata\Exception\InvalidArgumentException; @@ -142,7 +140,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT]; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?IdentifiersExtractorInterface $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -151,7 +149,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): LegacyIriConverterInterface|IriConverterInterface + protected function getIriConverter(): IriConverterInterface { return $this->iriConverter; } diff --git a/src/Doctrine/Odm/Tests/AppKernel.php b/src/Doctrine/Odm/Tests/AppKernel.php index 1b9481a50b2..0333f06741e 100644 --- a/src/Doctrine/Odm/Tests/AppKernel.php +++ b/src/Doctrine/Odm/Tests/AppKernel.php @@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; /** @@ -42,6 +43,12 @@ public function registerBundles(): array return [ new FrameworkBundle(), new DoctrineMongoDBBundle(), + new class extends Bundle { + public function shutdown(): void + { + restore_exception_handler(); + } + }, ]; } diff --git a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php index cce6e2d8d25..3641c4ace94 100644 --- a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php +++ b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php @@ -45,9 +45,7 @@ protected function setUp(): void $this->repository = $this->manager->getRepository($this->resourceClass); } - /** - * @dataProvider provideApplyTestData - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideApplyTestData')] public function testApply(?array $properties, array $filterParameters, array $expectedPipeline, ?callable $factory = null, ?string $resourceClass = null): void { $this->doTestApply($properties, $filterParameters, $expectedPipeline, $factory, $resourceClass); diff --git a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php index 0746d8665ff..3ab49d9f4cd 100644 --- a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php +++ b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php @@ -93,7 +93,7 @@ private static function createCacheConfiguration(bool $isDevMode, string $proxyD $namespace .= ':'; } - $cache->setNamespace($namespace.'dc2_'.md5($proxyDir.$hydratorDir).'_'); // to avoid collisions + $cache->setNamespace($namespace.'dc2_'.hash('xxh3', $proxyDir.$hydratorDir).'_'); // to avoid collisions return $cache; } diff --git a/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php index d90104ca55d..8c94abafb5a 100644 --- a/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php @@ -26,8 +26,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class FilterExtensionTest extends TestCase { diff --git a/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php index db157f582d8..f56aa8cb6e8 100644 --- a/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php @@ -28,8 +28,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class OrderExtensionTest extends TestCase { diff --git a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php index ed880c214df..7e28cfde308 100644 --- a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php @@ -37,8 +37,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class PaginationExtensionTest extends TestCase { diff --git a/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php b/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php index 4dce86ef25d..520f8517ad6 100644 --- a/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class BooleanFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php b/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php index 170082d17dd..e8ab45d62c2 100644 --- a/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php @@ -19,8 +19,6 @@ use MongoDB\BSON\UTCDateTime; /** - * @group mongodb - * * @author Alan Poulain */ class DateFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php b/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php index 0bf87b0a7fe..d6e987ce65b 100644 --- a/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php @@ -19,8 +19,6 @@ use Doctrine\Persistence\ManagerRegistry; /** - * @group mongodb - * * @author Alan Poulain */ class ExistsFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php b/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php index 3dd6b22d87f..db6e702afc0 100644 --- a/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class NumericFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php b/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php index 475b97795ea..24c6a7bad81 100644 --- a/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php @@ -21,8 +21,6 @@ use Doctrine\Persistence\ManagerRegistry; /** - * @group mongodb - * * @author Alan Poulain */ class OrderFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php b/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php index 51fd7744fe0..4863ae25840 100644 --- a/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class RangeFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php b/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php index 307c622145e..6a2f99e143b 100644 --- a/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php @@ -27,8 +27,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class SearchFilterTest extends DoctrineMongoDbOdmFilterTestCase { diff --git a/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php b/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php index 6b98b56a2d4..ac2f212ffc7 100644 --- a/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php +++ b/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php @@ -24,8 +24,6 @@ use Prophecy\PhpUnit\ProphecyTrait; /** - * @group mongodb - * * @author Alan Poulain */ class DoctrineMongoDbOdmPropertyMetadataFactoryTest extends TestCase diff --git a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php index b7c6e87183c..6b36f99053a 100644 --- a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php @@ -70,9 +70,7 @@ public function testWithoutManager(): void $this->assertNull($resourceMetadataCollection->getOperation('graphql_get')->getProvider()); } - /** - * @dataProvider operationProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { if (!class_exists(DocumentManager::class)) { diff --git a/src/Doctrine/Odm/Tests/PaginatorTest.php b/src/Doctrine/Odm/Tests/PaginatorTest.php index 08646af589b..d6540a7f46a 100644 --- a/src/Doctrine/Odm/Tests/PaginatorTest.php +++ b/src/Doctrine/Odm/Tests/PaginatorTest.php @@ -21,16 +21,11 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -/** - * @group mongodb - */ class PaginatorTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage, bool $hasNextPage): void { $paginator = $this->getPaginator($firstResult, $maxResults, $totalItems); diff --git a/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php index 08e247ef255..440e6e0c497 100644 --- a/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -31,8 +31,6 @@ use Symfony\Component\PropertyInfo\Type; /** - * @group mongodb - * * @author Kévin Dunglas * @author Alan Poulain */ @@ -84,9 +82,7 @@ public function testTestGetPropertiesWithEmbedded(): void ); } - /** - * @dataProvider typesProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('typesProvider')] public function testExtract(string $property, ?array $type = null): void { $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property)); diff --git a/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php b/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php index 5d81c5d90cf..832be3587ca 100644 --- a/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php +++ b/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php @@ -29,14 +29,9 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -/** - * @group mongodb - */ class CollectionProviderTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; private ObjectProphecy $managerRegistryProphecy; diff --git a/src/Doctrine/Odm/Tests/State/ItemProviderTest.php b/src/Doctrine/Odm/Tests/State/ItemProviderTest.php index 721c3fd8fa5..550ba7c1c5c 100644 --- a/src/Doctrine/Odm/Tests/State/ItemProviderTest.php +++ b/src/Doctrine/Odm/Tests/State/ItemProviderTest.php @@ -32,14 +32,9 @@ use Doctrine\Persistence\ObjectRepository; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -/** - * @group mongodb - */ class ItemProviderTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; public function testGetItemSingleIdentifier(): void diff --git a/src/Doctrine/Odm/composer.json b/src/Doctrine/Odm/composer.json index 59817a82e68..28e302fd5c9 100644 --- a/src/Doctrine/Odm/composer.json +++ b/src/Doctrine/Odm/composer.json @@ -24,7 +24,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/doctrine-common": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", @@ -32,14 +32,12 @@ "symfony/property-info": "^6.4 || ^7.1" }, "require-dev": { - "api-platform/parameter-validator": "^3.2", "doctrine/doctrine-bundle": "^2.11", "doctrine/mongodb-odm-bundle": "^5.0", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^10.0", "symfony/cache": "^6.4 || ^7.0", "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", "symfony/serializer": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0", @@ -66,7 +64,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index a94c8627ec0..d0caccb94d4 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; @@ -141,7 +139,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = Types::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?IdentifiersExtractorInterface $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -150,7 +148,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): IriConverterInterface|LegacyIriConverterInterface + protected function getIriConverter(): IriConverterInterface { return $this->iriConverter; } diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php index 1922ccdee44..bf958b80727 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Metadata\Resource; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Metadata; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -28,7 +27,7 @@ */ final class DoctrineOrmLinkFactory implements LinkFactoryInterface, PropertyLinkFactoryInterface { - public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly LinkFactoryInterface&PropertyLinkFactoryInterface $linkFactory) + public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly LinkFactoryInterface&PropertyLinkFactoryInterface $linkFactory) { } diff --git a/src/Doctrine/Orm/State/Options.php b/src/Doctrine/Orm/State/Options.php index 397d2da758a..3a9a46c3825 100644 --- a/src/Doctrine/Orm/State/Options.php +++ b/src/Doctrine/Orm/State/Options.php @@ -42,17 +42,4 @@ public function withEntityClass(?string $entityClass): self return $self; } - - public function getHandleLinks(): mixed - { - return $this->handleLinks; - } - - public function withHandleLinks(mixed $handleLinks): self - { - $self = clone $this; - $self->handleLinks = $handleLinks; - - return $self; - } } diff --git a/src/Doctrine/Orm/Tests/AppKernel.php b/src/Doctrine/Orm/Tests/AppKernel.php index 29229e2f5bd..94b796bb140 100644 --- a/src/Doctrine/Orm/Tests/AppKernel.php +++ b/src/Doctrine/Orm/Tests/AppKernel.php @@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; /** @@ -43,6 +44,12 @@ public function registerBundles(): array new FrameworkBundle(), new DoctrineBundle(), new TestBundle(), + new class extends Bundle { + public function shutdown(): void + { + restore_exception_handler(); + } + }, ]; } diff --git a/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php b/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php index f091da00923..1b8b25cfa10 100644 --- a/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php +++ b/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php @@ -45,9 +45,7 @@ protected function setUp(): void $this->repository = $this->managerRegistry->getManagerForClass(Dummy::class)->getRepository(Dummy::class); } - /** - * @dataProvider provideApplyTestData - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideApplyTestData')] public function testApply(?array $properties, array $filterParameters, string $expectedDql, ?array $expectedParameters = null, ?callable $factory = null, ?string $resourceClass = null): void { $this->doTestApply($properties, $filterParameters, $expectedDql, $expectedParameters, $factory, $resourceClass); diff --git a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php index 2bd173db6ca..5dec2ceea42 100644 --- a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php @@ -821,9 +821,7 @@ public function testApplyToCollectionWithANonReadableButFetchEagerProperty(): vo $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } - /** - * @dataProvider provideExistingJoinCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideExistingJoinCases')] public function testApplyToCollectionWithExistingJoin(string $joinType): void { $context = ['groups' => ['foo']]; diff --git a/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php index 64e505ce9bc..2b8bcc51142 100644 --- a/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php @@ -355,9 +355,7 @@ public function testGetResultWithoutDistinct(): void $this->assertFalse($query->getHint(CountWalker::HINT_DISTINCT)); } - /** - * @dataProvider fetchJoinCollectionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('fetchJoinCollectionProvider')] public function testGetResultWithFetchJoinCollection(bool $paginationFetchJoinCollection, array $context, bool $expected): void { $dummyMetadata = new ClassMetadata(Dummy::class); @@ -405,9 +403,7 @@ public static function fetchJoinCollectionProvider(): array ]; } - /** - * @dataProvider fetchUseOutputWalkersProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('fetchUseOutputWalkersProvider')] public function testGetResultWithUseOutputWalkers(bool $paginationUseOutputWalkers, array $context, bool $expected): void { $dummyMetadata = new ClassMetadata(Dummy::class); diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php index 9c1530cd8d9..f5a29cc0d03 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php @@ -27,7 +27,7 @@ * * @author Kévin Dunglas */ -#[ApiResource(filters: ['my_dummy.backed_enum', 'my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])] +#[ApiResource(filters: ['my_dummy.backed_enum', 'my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false])] #[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php index 3e95aa912b4..e0482b113df 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php @@ -24,7 +24,7 @@ /** * Related To Dummy Friend represent an association table for a manytomany relation. */ -#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] +#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] diff --git a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php index 3c16c0fbb6e..65b5eee9b12 100644 --- a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php +++ b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Tests\Metadata\Resource; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmLinkFactory; use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\Dummy; use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\RelatedDummy; @@ -25,6 +24,7 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\LinkFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\PropertyLinkFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; diff --git a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php index 9904186a2bc..3e371af885a 100644 --- a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php @@ -62,9 +62,7 @@ public function testWithoutManager(): void $this->assertNull($resourceMetadataCollection->getOperation('graphql_get')->getProvider()); } - /** - * @dataProvider operationProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { $objectManager = $this->prophesize(EntityManagerInterface::class); diff --git a/src/Doctrine/Orm/Tests/PaginatorTest.php b/src/Doctrine/Orm/Tests/PaginatorTest.php index b5a347a6b93..62010032848 100644 --- a/src/Doctrine/Orm/Tests/PaginatorTest.php +++ b/src/Doctrine/Orm/Tests/PaginatorTest.php @@ -30,9 +30,7 @@ class PaginatorTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage, bool $hasNextPage): void { $paginator = $this->getPaginator($firstResult, $maxResults, $totalItems); diff --git a/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php b/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php index 7ce2b446b99..488d217743f 100644 --- a/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php +++ b/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php @@ -29,9 +29,7 @@ class QueryBuilderHelperTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider provideAddJoinOnce - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideAddJoinOnce')] public function testAddJoinOnce(?string $originAliasForJoinOnce, string $expectedAlias): void { $queryBuilder = new QueryBuilder($this->prophesize(EntityManagerInterface::class)->reveal()); @@ -57,9 +55,7 @@ public function testAddJoinOnce(?string $originAliasForJoinOnce, string $expecte $queryBuilder->getDQLPart('join')[$originAliasForJoinOnce ?? 'f'][0]->getAlias()); } - /** - * @dataProvider provideAddJoinOnce - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideAddJoinOnce')] public function testAddJoinOnceWithSpecifiedNewAlias(): void { $queryBuilder = new QueryBuilder($this->prophesize(EntityManagerInterface::class)->reveal()); diff --git a/src/Doctrine/Orm/composer.json b/src/Doctrine/Orm/composer.json index 2ec46bc6771..ff2b513fb42 100644 --- a/src/Doctrine/Orm/composer.json +++ b/src/Doctrine/Orm/composer.json @@ -23,15 +23,14 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/doctrine-common": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", "doctrine/orm": "^2.17 || ^3.0", - "symfony/property-info": "^6.4 || ^7.1" + "symfony/property-info": "^6.4 || ^7.0" }, "require-dev": { - "api-platform/parameter-validator": "^3.2", "doctrine/doctrine-bundle": "^2.11", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^10.0", @@ -39,7 +38,6 @@ "ramsey/uuid-doctrine": "^2.0", "symfony/cache": "^6.4 || ^7.0", "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", "symfony/serializer": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0", @@ -66,7 +64,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Documentation/Action/EntrypointAction.php b/src/Documentation/Action/EntrypointAction.php index 70677c59821..d93e1d9bb16 100644 --- a/src/Documentation/Action/EntrypointAction.php +++ b/src/Documentation/Action/EntrypointAction.php @@ -43,7 +43,7 @@ public function __construct( public function __invoke(Request $request) { - static::$resourceNameCollection = $this->resourceNameCollectionFactory->create(); + self::$resourceNameCollection = $this->resourceNameCollectionFactory->create(); $context = [ 'request' => $request, 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), @@ -65,6 +65,6 @@ class: Entrypoint::class, public static function provide(): Entrypoint { - return new Entrypoint(static::$resourceNameCollection); + return new Entrypoint(self::$resourceNameCollection); } } diff --git a/src/Documentation/composer.json b/src/Documentation/composer.json index f1359f6b86f..71c570c8c48 100644 --- a/src/Documentation/composer.json +++ b/src/Documentation/composer.json @@ -20,6 +20,7 @@ } ], "require": { + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0" }, "extra": { @@ -28,11 +29,14 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", "url": "https://github.com/api-platform/api-platform" } + }, + "require-dev": { + "phpunit/phpunit": "^11.2" } } diff --git a/src/Elasticsearch/Metadata/Document/DocumentMetadata.php b/src/Elasticsearch/Metadata/Document/DocumentMetadata.php deleted file mode 100644 index 7911865e99d..00000000000 --- a/src/Elasticsearch/Metadata/Document/DocumentMetadata.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document; - -/** - * Document metadata. - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html - * @deprecated - * - * @author Baptiste Meyer - */ -final class DocumentMetadata -{ - public const DEFAULT_TYPE = '_doc'; - - public function __construct(private ?string $index = null, private string $type = self::DEFAULT_TYPE) - { - } - - /** - * Gets a new instance with the given index. - */ - public function withIndex(string $index): self - { - $metadata = clone $this; - $metadata->index = $index; - - return $metadata; - } - - /** - * Gets the document index. - */ - public function getIndex(): ?string - { - return $this->index; - } - - /** - * Gets a new instance with the given type. - */ - public function withType(string $type): self - { - $metadata = clone $this; - $metadata->type = $type; - - return $metadata; - } - - /** - * Gets the document type. - */ - public function getType(): string - { - return $this->type; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php deleted file mode 100644 index 5475bc90b7c..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; - -/** - * Creates document's metadata using the attribute configuration. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class AttributeDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - $resourceMetadata = null; - - if (!$documentMetadata || null === $documentMetadata->getIndex()) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - $index = $resourceMetadata->getOperation()->getExtraProperties()['elasticsearch_index'] ?? null; - - if (null !== $index) { - $documentMetadata = $documentMetadata ? $documentMetadata->withIndex($index) : new DocumentMetadata($index); - } - } - - if (!$documentMetadata || DocumentMetadata::DEFAULT_TYPE === $documentMetadata->getType()) { - $resourceMetadata ??= $this->resourceMetadataFactory->create($resourceClass); - $type = $resourceMetadata->getOperation()->getExtraProperties()['elasticsearch_type'] ?? null; - - if (null !== $type) { - $documentMetadata = $documentMetadata ? $documentMetadata->withType($type) : new DocumentMetadata(null, $type); - } - } - - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php deleted file mode 100644 index bc78eab486d..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use Psr\Cache\CacheException; -use Psr\Cache\CacheItemPoolInterface; - -/** - * Caches document metadata. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class CachedDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - private const CACHE_KEY_PREFIX = 'index_metadata'; - private array $localCache = []; - - public function __construct(private readonly CacheItemPoolInterface $cacheItemPool, private readonly DocumentMetadataFactoryInterface $decorated) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - if (isset($this->localCache[$resourceClass])) { - return $this->handleNotFound($this->localCache[$resourceClass], $resourceClass); - } - - try { - $cacheItem = $this->cacheItemPool->getItem(self::CACHE_KEY_PREFIX.md5($resourceClass)); - } catch (CacheException) { - return $this->handleNotFound($this->localCache[$resourceClass] = $this->decorated->create($resourceClass), $resourceClass); - } - - if ($cacheItem->isHit()) { - return $this->handleNotFound($this->localCache[$resourceClass] = $cacheItem->get(), $resourceClass); - } - - $documentMetadata = $this->decorated->create($resourceClass); - - $cacheItem->set($documentMetadata); - $this->cacheItemPool->save($cacheItem); - - return $this->handleNotFound($this->localCache[$resourceClass] = $documentMetadata, $resourceClass); - } - - /** - * @throws IndexNotFoundException - */ - private function handleNotFound(DocumentMetadata $documentMetadata, string $resourceClass): DocumentMetadata - { - if (null === $documentMetadata->getIndex()) { - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } - - return $documentMetadata; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php deleted file mode 100644 index dba833262cb..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Metadata\InflectorInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\Inflector; -use Elastic\Elasticsearch\Exception\ClientResponseException; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; - -/** - * Creates document's metadata using indices from the cat APIs. - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html - * @deprecated - * - * @author Baptiste Meyer - */ -final class CatDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - // @phpstan-ignore-next-line - public function __construct(private readonly Client $client, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null, private readonly ?InflectorInterface $inflector = new Inflector()) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - if ($documentMetadata && null !== $documentMetadata->getIndex()) { - return $documentMetadata; - } - - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - $resourceShortName = $resourceMetadata->getOperation()->getShortName(); - - if (null === $resourceShortName) { - return $this->handleNotFound($documentMetadata, $resourceClass); - } - - $index = $this->inflector->tableize($resourceShortName); - - try { - // @phpstan-ignore-next-line - $this->client->cat()->indices(['index' => $index]); - // @phpstan-ignore-next-line - } catch (Missing404Exception|ClientResponseException) { - return $this->handleNotFound($documentMetadata, $resourceClass); - } - - return ($documentMetadata ?? new DocumentMetadata())->withIndex($index); - } - - /** - * @throws IndexNotFoundException - */ - private function handleNotFound(?DocumentMetadata $documentMetadata, string $resourceClass): DocumentMetadata - { - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php deleted file mode 100644 index 5632b421a52..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; - -/** - * Creates document's metadata using the mapping configuration. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class ConfiguredDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - public function __construct(private readonly array $mapping, private readonly ?DocumentMetadataFactoryInterface $decorated = null) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - if (null === $index = $this->mapping[$resourceClass] ?? null) { - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } - - $documentMetadata ??= new DocumentMetadata(); - - if (isset($index['index'])) { - $documentMetadata = $documentMetadata->withIndex($index['index']); - } - - if (isset($index['type'])) { - $documentMetadata = $documentMetadata->withType($index['type']); - } - - return $documentMetadata; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php b/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php deleted file mode 100644 index 89f0da092da..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; - -/** - * Creates a document metadata value object. - * - * @deprecated - * - * @author Baptiste Meyer - */ -interface DocumentMetadataFactoryInterface -{ - /** - * Creates document metadata. - * - * @throws IndexNotFoundException - */ - public function create(string $resourceClass): DocumentMetadata; -} diff --git a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php index 13042a677c3..f15819a1031 100644 --- a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php +++ b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php @@ -17,21 +17,13 @@ use ApiPlatform\Elasticsearch\State\ItemProvider; use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\InflectorInterface; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Common\Exceptions\NoNodesAvailableException; final class ElasticsearchProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly ?Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true, private readonly ?InflectorInterface $inflector = null) // @phpstan-ignore-line + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated) { - if ($client) { - trigger_deprecation('api-platform/core', '4.0', \sprintf('Using $client at "%s" is deprecated and the argument will be removed.', self::class)); - } } /** @@ -50,18 +42,7 @@ public function create(string $resourceClass): ResourceMetadataCollection continue; } - if (null !== ($elasticsearch = $operation->getElasticsearch())) { - trigger_deprecation('api-platform/core', '3.1', \sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class)); - } - - $hasElasticsearch = true === $elasticsearch || $operation->getStateOptions() instanceof Options; - - // Old behavior in ES < 8 - if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line - $hasElasticsearch = true; - } - - if (!$hasElasticsearch) { + if (!$operation->getStateOptions() instanceof Options) { continue; } @@ -79,18 +60,7 @@ public function create(string $resourceClass): ResourceMetadataCollection continue; } - if (null !== ($elasticsearch = $graphQlOperation->getElasticsearch())) { - trigger_deprecation('api-platform/core', '3.1', \sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class)); - } - - $hasElasticsearch = true === $elasticsearch || $graphQlOperation->getStateOptions() instanceof Options; - - // Old behavior in ES < 8 - if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line - $hasElasticsearch = true; - } - - if (!$hasElasticsearch) { + if (!$graphQlOperation->getStateOptions() instanceof Options) { continue; } @@ -105,18 +75,4 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } - - private function hasIndices(Operation $operation): bool - { - $shortName = $operation->getShortName(); - $index = $this->inflector->tableize($shortName); - - try { - $this->client->cat()->indices(['index' => $index]); // @phpstan-ignore-line - - return true; - } catch (Missing404Exception|NoNodesAvailableException) { // @phpstan-ignore-line - return false; - } - } } diff --git a/src/Elasticsearch/Paginator.php b/src/Elasticsearch/Paginator.php index 2635be0b62d..2a1c6edc70e 100644 --- a/src/Elasticsearch/Paginator.php +++ b/src/Elasticsearch/Paginator.php @@ -95,7 +95,7 @@ public function getIterator(): \Traversable $denormalizationContext = array_merge([AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true], $this->denormalizationContext); foreach ($this->documents['hits']['hits'] ?? [] as $document) { - $cacheKey = isset($document['_index'], $document['_id']) ? md5("{$document['_index']}_{$document['_id']}") : null; + $cacheKey = isset($document['_index'], $document['_id']) ? hash('xxh3', "{$document['_index']}_{$document['_id']}") : null; if ($cacheKey && \array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) { $object = $this->cachedDenormalizedDocuments[$cacheKey]; diff --git a/src/Elasticsearch/Serializer/ItemNormalizer.php b/src/Elasticsearch/Serializer/ItemNormalizer.php index 5bec2e647ca..a33c4ab48b8 100644 --- a/src/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Elasticsearch/Serializer/ItemNormalizer.php @@ -13,12 +13,9 @@ namespace ApiPlatform\Elasticsearch\Serializer; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -28,7 +25,7 @@ * * @experimental */ -final class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface +final class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface { public const FORMAT = 'elasticsearch'; @@ -36,27 +33,6 @@ public function __construct(private readonly NormalizerInterface $decorated) { } - /** - * @throws LogicException - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - if (!$this->decorated instanceof BaseCacheableSupportsMethodInterface) { - throw new LogicException(\sprintf('The decorated normalizer must be an instance of "%s".', BaseCacheableSupportsMethodInterface::class)); - } - - return $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} * @@ -107,7 +83,6 @@ public function getSupportedTypes($format): array if (!method_exists($this->decorated, 'getSupportedTypes')) { return [ DocumentNormalizer::FORMAT => null, - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), ]; } diff --git a/src/Elasticsearch/State/CollectionProvider.php b/src/Elasticsearch/State/CollectionProvider.php index bce0b8e6a4f..e4ade908821 100644 --- a/src/Elasticsearch/State/CollectionProvider.php +++ b/src/Elasticsearch/State/CollectionProvider.php @@ -14,8 +14,6 @@ namespace ApiPlatform\Elasticsearch\State; use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; use ApiPlatform\Elasticsearch\Paginator; use ApiPlatform\Metadata\InflectorInterface; use ApiPlatform\Metadata\Operation; @@ -26,7 +24,6 @@ use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Response\Elasticsearch; -use Elasticsearch\Client as LegacyClient; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** @@ -40,7 +37,7 @@ final class CollectionProvider implements ProviderInterface /** * @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions */ - public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?Pagination $pagination = null, private readonly iterable $collectionExtensions = [], private readonly ?InflectorInterface $inflector = new Inflector()) // @phpstan-ignore-line + public function __construct(private readonly Client $client, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?Pagination $pagination = null, private readonly iterable $collectionExtensions = [], private readonly ?InflectorInterface $inflector = new Inflector()) { } @@ -65,18 +62,13 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation)); - // TODO: remove in 4.x - if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) { - $options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass)); - } - $params = [ 'index' => $options->getIndex() ?? $this->getIndex($operation), 'body' => $body, ]; try { - $documents = $this->client->search($params); // @phpstan-ignore-line + $documents = $this->client->search($params); } catch (ClientResponseException $e) { $response = $e->getResponse(); throw new Error(status: $response->getStatusCode(), detail: (string) $response->getBody(), title: $response->getReasonPhrase(), originalTrace: $e->getTrace()); @@ -96,11 +88,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c ); } - private function convertDocumentMetadata(DocumentMetadata $documentMetadata): Options - { - return new Options($documentMetadata->getIndex(), $documentMetadata->getType()); - } - private function getIndex(Operation $operation): string { return $this->inflector->tableize($operation->getShortName()); diff --git a/src/Elasticsearch/State/ItemProvider.php b/src/Elasticsearch/State/ItemProvider.php index 1511be17243..7828145f89d 100644 --- a/src/Elasticsearch/State/ItemProvider.php +++ b/src/Elasticsearch/State/ItemProvider.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Elasticsearch\State; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer; use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\InflectorInterface; @@ -25,8 +23,6 @@ use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Response\Elasticsearch; -use Elasticsearch\Client as LegacyClient; -use Elasticsearch\Common\Exceptions\Missing404Exception; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -38,7 +34,7 @@ */ final class ItemProvider implements ProviderInterface { - public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?InflectorInterface $inflector = new Inflector()) // @phpstan-ignore-line + public function __construct(private readonly Client $client, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?InflectorInterface $inflector = new Inflector()) { } @@ -48,14 +44,7 @@ public function __construct(private readonly LegacyClient|Client $client, privat public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { $resourceClass = $operation->getClass(); - $options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation)); - - // TODO: remove in 4.x - if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) { - $options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass)); - } - if (!$options instanceof Options) { throw new RuntimeException(\sprintf('The "%s" provider was called without "%s".', self::class, Options::class)); } @@ -66,9 +55,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c ]; try { - $document = $this->client->get($params); // @phpstan-ignore-line - } catch (Missing404Exception) { // @phpstan-ignore-line - return null; + $document = $this->client->get($params); } catch (ClientResponseException $e) { $response = $e->getResponse(); if (404 === $response->getStatusCode()) { @@ -90,11 +77,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $item; } - private function convertDocumentMetadata(DocumentMetadata $documentMetadata): Options - { - return new Options($documentMetadata->getIndex(), $documentMetadata->getType()); - } - private function getIndex(Operation $operation): string { return $this->inflector->tableize($operation->getShortName()); diff --git a/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php b/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php deleted file mode 100644 index a7c304bb9d0..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document; - -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use PHPUnit\Framework\TestCase; - -class DocumentMetadataTest extends TestCase -{ - public function testValueObject(): void - { - $documentMetadataOne = new DocumentMetadata('foo', 'bar'); - - self::assertSame('foo', $documentMetadataOne->getIndex()); - self::assertSame('bar', $documentMetadataOne->getType()); - - $documentMetadataTwo = $documentMetadataOne->withIndex('baz'); - - self::assertNotSame($documentMetadataTwo, $documentMetadataOne); - self::assertSame('baz', $documentMetadataTwo->getIndex()); - self::assertSame('bar', $documentMetadataTwo->getType()); - - $documentMetadataThree = $documentMetadataTwo->withType(DocumentMetadata::DEFAULT_TYPE); - - self::assertNotSame($documentMetadataThree, $documentMetadataOne); - self::assertNotSame($documentMetadataThree, $documentMetadataTwo); - self::assertSame('baz', $documentMetadataThree->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadataThree->getType()); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php deleted file mode 100644 index a39d31bee6f..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\AttributeDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class AttributeDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new AttributeDocumentMetadataFactory( - $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([(new Get())->withExtraProperties(['elasticsearch_index' => 'foo', 'elasticsearch_type' => 'bar'])]))]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $documentMetadata = (new AttributeDocumentMetadataFactory($resourceMetadataFactoryProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertNotSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame('bar', $documentMetadata->getType()); - } - - public function testCreateWithNoParentDocumentMetadataAndNoAttributes(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get()]))]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - (new AttributeDocumentMetadataFactory($resourceMetadataFactoryProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php deleted file mode 100644 index b5187c68cfd..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,159 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\CachedDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Exception\CacheException; - -class CachedDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new CachedDocumentMetadataFactory( - $this->prophesize(CacheItemPoolInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalled(); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithLocalCache(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalledTimes(1); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalledTimes(1); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalledTimes(1); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalledTimes(1); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalledTimes(1); - - $documentMetadataFactory = new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()); - $documentMetadataFactory->create(Foo::class); - - $documentMetadata = $documentMetadataFactory->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithCacheException(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->shouldNotBeCalled(); - $cacheItemProphecy->set(Argument::any())->shouldNotBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willThrow(new CacheException())->shouldBeCalledTimes(1); - $cacheItemPoolProphecy->save(Argument::any())->shouldNotBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithCacheHit(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(true)->shouldBeCalled(); - $cacheItemProphecy->get()->willReturn($originalDocumentMetadata)->shouldBeCalled(); - $cacheItemProphecy->set(Argument::any())->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save(Argument::any())->shouldNotBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Argument::any())->shouldNotBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexNotDefined(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $originalDocumentMetadata = new DocumentMetadata(); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalled(); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php deleted file mode 100644 index 95b6028c324..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,145 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\CatDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Namespaces\CatNamespace; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class CatDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - protected function setUp(): void - { - if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) { - $this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.'); - } - } - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new CatDocumentMetadataFactory( - $this->prophesize(Client::class)->reveal(), - $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get(shortName: 'Foo')]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $catNamespaceProphecy = $this->prophesize(CatNamespace::class); - $catNamespaceProphecy->indices(['index' => 'foo']) - ->willReturn([[ - 'health' => 'yellow', - 'status' => 'open', - 'index' => 'foo', - 'uuid' => '123456789abcdefghijklmn', - 'pri' => '5', - 'rep' => '1', - 'docs.count' => '42', - 'docs.deleted' => '0', - 'store.size' => '42kb', - 'pri.store.size' => '42kb', - ]]) - ->shouldBeCalled(); - - $clientProphecy = $this->prophesize(Client::class); - $clientProphecy->cat()->willReturn($catNamespaceProphecy)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($clientProphecy->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexAlreadySet(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($this->prophesize(Client::class)->reveal(), $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithNoResourceShortName(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get()]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($this->prophesize(Client::class)->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertNull($documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexNotFound(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get(shortName: 'Foo')]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $catNamespaceProphecy = $this->prophesize(CatNamespace::class); - // @phpstan-ignore-next-line - $catNamespaceProphecy->indices(['index' => 'foo'])->willThrow(new Missing404Exception())->shouldBeCalled(); - - $clientProphecy = $this->prophesize(Client::class); - $clientProphecy->cat()->willReturn($catNamespaceProphecy)->shouldBeCalled(); - - (new CatDocumentMetadataFactory($clientProphecy->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php deleted file mode 100644 index 772450d3a88..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\ConfiguredDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class ConfiguredDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new ConfiguredDocumentMetadataFactory([], $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal()) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $configuredDocumentMetadata = (new ConfiguredDocumentMetadataFactory([Foo::class => ['index' => 'foo', 'type' => 'bar']], $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertNotSame($originalDocumentMetadata, $configuredDocumentMetadata); - self::assertSame('foo', $configuredDocumentMetadata->getIndex()); - self::assertSame('bar', $configuredDocumentMetadata->getType()); - } - - public function testCreateWithEmptyMapping(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $configuredDocumentMetadata = (new ConfiguredDocumentMetadataFactory([], $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $configuredDocumentMetadata); - } - - public function testCreateWithEmptyMappingAndNoParentDocumentMetadata(): void - { - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - (new ConfiguredDocumentMetadataFactory([], $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php index 4a7a22b579b..f9648876273 100644 --- a/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php +++ b/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php @@ -14,22 +14,16 @@ namespace ApiPlatform\Elasticsearch\Tests\Metadata\Resource\Factory; use ApiPlatform\Elasticsearch\Metadata\Resource\Factory\ElasticsearchProviderResourceMetadataCollectionFactory; -use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Tests\Fixtures\Metadata\Get; -use Elasticsearch\Client as LegacyClient; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Namespaces\CatNamespace; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; class ElasticsearchProviderResourceMetadataCollectionFactoryTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; public function testConstruct(): void @@ -37,27 +31,14 @@ public function testConstruct(): void self::assertInstanceOf( ResourceMetadataCollectionFactoryInterface::class, new ElasticsearchProviderResourceMetadataCollectionFactory( - class_exists(LegacyClient::class) ? $this->prophesize(LegacyClient::class)->reveal() : null, $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal() ) ); } - /** - * @dataProvider elasticsearchProvider - */ - public function testCreate(?bool $elasticsearchFlag, int $expectedCatCallCount, ?bool $expectedResult): void + #[\PHPUnit\Framework\Attributes\DataProvider('elasticsearchProvider')] + public function testCreate(?bool $elasticsearchFlag, ?bool $expectedResult): void { - if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) { - $this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.'); - } - - if (null !== $elasticsearchFlag) { - $solution = $elasticsearchFlag - ? \sprintf('Pass an instance of %s to $stateOptions instead', Options::class) - : 'You will have to remove it when upgrading to v4'; - $this->expectDeprecation(\sprintf('Since api-platform/core 3.1: Setting "elasticsearch" in Operation is deprecated. %s', $solution)); - } $get = (new Get(elasticsearch: $elasticsearchFlag, shortName: 'Foo')); $resource = (new ApiResource(operations: ['foo_get' => $get])); $metadata = new ResourceMetadataCollection(Foo::class, [$resource]); @@ -65,31 +46,7 @@ public function testCreate(?bool $elasticsearchFlag, int $expectedCatCallCount, $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $decorated->create(Foo::class)->willReturn($metadata)->shouldBeCalled(); - // @phpstan-ignore-next-line - $catNamespace = $this->prophesize(CatNamespace::class); - if ($elasticsearchFlag) { - $catNamespace->indices(['index' => 'foo'])->willReturn([[ - 'health' => 'yellow', - 'status' => 'open', - 'index' => 'foo', - 'uuid' => '123456789abcdefghijklmn', - 'pri' => '5', - 'rep' => '1', - 'docs.count' => '42', - 'docs.deleted' => '0', - 'store.size' => '42kb', - 'pri.store.size' => '42kb', - ]]); - } else { - // @phpstan-ignore-next-line - $catNamespace->indices(['index' => 'foo'])->willThrow(new Missing404Exception()); - } - - // @phpstan-ignore-next-line - $client = $this->prophesize(LegacyClient::class); - $client->cat()->willReturn($catNamespace)->shouldBeCalledTimes($expectedCatCallCount); - - $resourceMetadataFactory = new ElasticsearchProviderResourceMetadataCollectionFactory($client->reveal(), $decorated->reveal(), false); + $resourceMetadataFactory = new ElasticsearchProviderResourceMetadataCollectionFactory($decorated->reveal()); $elasticsearchResult = $resourceMetadataFactory->create(Foo::class)->getOperation('foo_get')->getElasticsearch(); self::assertEquals($expectedResult, $elasticsearchResult); } @@ -97,9 +54,9 @@ public function testCreate(?bool $elasticsearchFlag, int $expectedCatCallCount, public static function elasticsearchProvider(): array { return [ - 'elasticsearch: false' => [false, 0, false], - 'elasticsearch: null' => [null, 1, false], - 'elasticsearch: true' => [true, 1, true], + 'elasticsearch: false' => [false, false], + 'elasticsearch: null' => [null, false], + 'elasticsearch: true' => [true, true], ]; } } diff --git a/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php b/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php index e0994c9557a..7819eb3951c 100644 --- a/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php +++ b/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php @@ -19,10 +19,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -40,10 +38,6 @@ protected function setUp(): void ->willImplement(DenormalizerInterface::class) ->willImplement(SerializerAwareInterface::class); - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->normalizerProphecy->willImplement(CacheableSupportsMethodInterface::class); - } - $this->itemNormalizer = new ItemNormalizer($this->normalizerProphecy->reveal()); } @@ -54,20 +48,6 @@ public function testConstruct(): void self::assertInstanceOf(SerializerAwareInterface::class, $this->itemNormalizer); } - /** - * @group legacy - */ - public function testHasCacheableSupportsMethod(): void - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - $this->markTestSkipped('Symfony Serializer >= 6.3'); - } - - $this->normalizerProphecy->hasCacheableSupportsMethod()->willReturn(true)->shouldBeCalledOnce(); - - self::assertTrue($this->itemNormalizer->hasCacheableSupportsMethod()); - } - public function testDenormalize(): void { $this->normalizerProphecy->denormalize('foo', 'string', 'json', ['groups' => 'foo'])->willReturn('foo')->shouldBeCalledOnce(); @@ -107,17 +87,6 @@ public function testSetSerializer(): void $this->itemNormalizer->setSerializer($serializer); } - /** - * @group legacy - */ - public function testHasCacheableSupportsMethodWithDecoratedNormalizerNotAnInstanceOfCacheableSupportsMethodInterface(): void - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage(\sprintf('The decorated normalizer must be an instance of "%s".', CacheableSupportsMethodInterface::class)); - - (new ItemNormalizer($this->prophesize(NormalizerInterface::class)->reveal()))->hasCacheableSupportsMethod(); - } - public function testDenormalizeWithDecoratedNormalizerNotAnInstanceOfDenormalizerInterface(): void { $this->expectException(LogicException::class); @@ -144,10 +113,6 @@ public function testSetSerializerWithDecoratedNormalizerNotAnInstanceOfSerialize public function testGetSupportedTypes(): void { - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->markTestSkipped('Symfony Serializer < 6.3'); - } - // TODO: use prophecy when getSupportedTypes() will be added to the interface $this->itemNormalizer = new ItemNormalizer(new class implements NormalizerInterface { public function normalize(mixed $object, ?string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null diff --git a/src/Elasticsearch/Tests/State/CollectionProviderTest.php b/src/Elasticsearch/Tests/State/CollectionProviderTest.php deleted file mode 100644 index 6eec78c033b..00000000000 --- a/src/Elasticsearch/Tests/State/CollectionProviderTest.php +++ /dev/null @@ -1,148 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\State; - -use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Paginator; -use ApiPlatform\Elasticsearch\State\CollectionProvider; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\Pagination\Pagination; -use Elastic\Elasticsearch\ClientInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Baptiste Meyer - * @author Vincent Chalamon - */ -final class CollectionProviderTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - self::assertInstanceOf( - CollectionProvider::class, - new CollectionProvider( - $this->prophesize(\Elasticsearch\Client::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(), - $this->prophesize(DenormalizerInterface::class)->reveal(), - new Pagination() - ) - ); - } - - public function testGetCollection(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $context = [ - 'groups' => ['custom'], - ]; - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadataCollection(Foo::class)); - - $documents = [ - 'took' => 15, - 'time_out' => false, - '_shards' => [ - 'total' => 5, - 'successful' => 5, - 'skipped' => 0, - 'failed' => 0, - ], - 'hits' => [ - 'total' => 4, - 'max_score' => 1, - 'hits' => [ - [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '1', - '_score' => 1, - '_source' => [ - 'id' => 1, - 'name' => 'Kilian', - 'bar' => 'Jornet', - ], - ], - [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '2', - '_score' => 1, - '_source' => [ - 'id' => 2, - 'name' => 'François', - 'bar' => 'D\'Haene', - ], - ], - ], - ], - ]; - - $clientProphecy = $this->prophesize(\Elasticsearch\Client::class); - $clientProphecy - ->search( - Argument::allOf( - Argument::withEntry('index', 'foo'), - Argument::withEntry('body', Argument::allOf( - Argument::withEntry('size', 2), - Argument::withEntry('from', 0), - Argument::withEntry('query', Argument::allOf( - Argument::withEntry('match_all', Argument::type(\stdClass::class)), - Argument::size(1) - )), - Argument::size(3) - )), - Argument::size(2) - ) - ) - ->willReturn($documents) - ->shouldBeCalled(); - - $operation = (new Get())->withName('get')->withClass(Foo::class)->withShortName('foo'); - - $requestBodySearchCollectionExtensionProphecy = $this->prophesize(RequestBodySearchCollectionExtensionInterface::class); - $requestBodySearchCollectionExtensionProphecy->applyToCollection([], Foo::class, $operation, $context)->willReturn([])->shouldBeCalled(); - - $provider = new CollectionProvider( - $clientProphecy->reveal(), - $documentMetadataFactoryProphecy->reveal(), - $denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal(), - new Pagination(['items_per_page' => 2]), - [$requestBodySearchCollectionExtensionProphecy->reveal()] - ); - - self::assertEquals( - new Paginator($denormalizer, $documents, Foo::class, 2, 0, $context), - $provider->provide($operation, [], $context) - ); - } -} diff --git a/src/Elasticsearch/Tests/State/ItemProviderTest.php b/src/Elasticsearch/Tests/State/ItemProviderTest.php deleted file mode 100644 index 0a260acef4b..00000000000 --- a/src/Elasticsearch/Tests/State/ItemProviderTest.php +++ /dev/null @@ -1,108 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\State; - -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer; -use ApiPlatform\Elasticsearch\State\ItemProvider; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\Get; -use Elastic\Elasticsearch\ClientInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Baptiste Meyer - * @author Vincent Chalamon - */ -final class ItemProviderTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - self::assertInstanceOf( - ItemProvider::class, - new ItemProvider( - $this->prophesize(\Elasticsearch\Client::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(), - $this->prophesize(DenormalizerInterface::class)->reveal() - ) - ); - } - - public function testGetItem(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $document = [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '1', - 'found' => true, - '_source' => [ - 'id' => 1, - 'name' => 'Rossinière', - 'bar' => 'erèinissor', - ], - ]; - - $foo = new Foo(); - $foo->setName('Rossinière'); - $foo->setBar('erèinissor'); - - $clientProphecy = $this->prophesize(\Elasticsearch\Client::class); - $clientProphecy->get(['index' => 'foo', 'id' => '1'])->willReturn($document)->shouldBeCalled(); - - $denormalizerProphecy = $this->prophesize(DenormalizerInterface::class); - $denormalizerProphecy->denormalize($document, Foo::class, DocumentNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true])->willReturn($foo)->shouldBeCalled(); - - $itemDataProvider = new ItemProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $denormalizerProphecy->reveal()); - - self::assertSame($foo, $itemDataProvider->provide((new Get())->withClass(Foo::class)->withShortName('foo'), ['id' => 1])); - } - - public function testGetInexistantItem(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $clientClass = class_exists(\Elasticsearch\Client::class) ? \Elasticsearch\Client::class : ClientInterface::class; - - $clientProphecy = $this->prophesize($clientClass); - $clientProphecy->get(['index' => 'foo', 'id' => '404'])->willReturn([ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '404', - 'found' => false, - ])->shouldBeCalled(); - - $itemDataProvider = new ItemProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $this->prophesize(DenormalizerInterface::class)->reveal()); - - self::assertNull($itemDataProvider->provide((new Get())->withClass(Foo::class)->withShortName('foo'), ['id' => 404])); - } -} diff --git a/src/Elasticsearch/composer.json b/src/Elasticsearch/composer.json index 871123fed22..7d4961afef3 100644 --- a/src/Elasticsearch/composer.json +++ b/src/Elasticsearch/composer.json @@ -23,22 +23,21 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/serializer": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", - "elasticsearch/elasticsearch": "^7.11 || ^8.9", + "elasticsearch/elasticsearch": "^8.4", "symfony/cache": "^6.4 || ^7.0", "symfony/console": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -65,7 +64,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Elasticsearch/phpunit.xml.dist b/src/Elasticsearch/phpunit.xml.dist index 0b1b38b4a5b..182d2943658 100644 --- a/src/Elasticsearch/phpunit.xml.dist +++ b/src/Elasticsearch/phpunit.xml.dist @@ -1,31 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Exception/DeserializationException.php b/src/Exception/DeserializationException.php deleted file mode 100644 index 1cc3584ad48..00000000000 --- a/src/Exception/DeserializationException.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface; - -/** - * Deserialization exception. - * - * @author Samuel ROZE - * @author Kévin Dunglas - * - * @deprecated - */ -class DeserializationException extends \Exception implements ExceptionInterface, SerializerExceptionInterface -{ -} diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php deleted file mode 100644 index c094cc44ed4..00000000000 --- a/src/Exception/ExceptionInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Base exception interface. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Exception\ExceptionInterface - */ -interface ExceptionInterface extends \Throwable -{ -} diff --git a/src/Exception/FilterValidationException.php b/src/Exception/FilterValidationException.php deleted file mode 100644 index e5008b430c0..00000000000 --- a/src/Exception/FilterValidationException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -use ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface; - -/** - * Filter validation exception. - * - * @author Julien DENIAU - * - * @deprecated use \ApiPlatform\ParameterValidator\Exception\ValidationException instead - */ -final class FilterValidationException extends \Exception implements ValidationExceptionInterface, ExceptionInterface, \Stringable -{ - public function __construct(private readonly array $constraintViolationList, string $message = '', int $code = 0, ?\Exception $previous = null) - { - parent::__construct($message ?: $this->__toString(), $code, $previous); - } - - public function __toString(): string - { - return implode("\n", $this->constraintViolationList); - } -} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php deleted file mode 100644 index 57cf4bb3d9b..00000000000 --- a/src/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Invalid argument exception. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Exception\InvalidArgumentException - */ -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidIdentifierException.php b/src/Exception/InvalidIdentifierException.php deleted file mode 100644 index 3d22df6058e..00000000000 --- a/src/Exception/InvalidIdentifierException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - * - * @deprecated use ApiPlatform\Metadata\Exception\InvalidIdentifierException - */ -class InvalidIdentifierException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidUriVariableException.php b/src/Exception/InvalidUriVariableException.php deleted file mode 100644 index e686cec932a..00000000000 --- a/src/Exception/InvalidUriVariableException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - * - * @deprecated use ApiPlatform\Metadata\Exception\InvalidUriVariableException - */ -class InvalidUriVariableException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/ItemNotFoundException.php b/src/Exception/ItemNotFoundException.php deleted file mode 100644 index b21d2732ecc..00000000000 --- a/src/Exception/ItemNotFoundException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Item not found exception. - * - * @author Amrouche Hamza - * - * @deprecated use ApiPlatform\Metadata\Exception\ItemNotFoundException - */ -class ItemNotFoundException extends InvalidArgumentException -{ -} diff --git a/src/Exception/NotExposedHttpException.php b/src/Exception/NotExposedHttpException.php deleted file mode 100644 index 08d50f22982..00000000000 --- a/src/Exception/NotExposedHttpException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * @author Vincent Chalamon - * - * @deprecated use ApiPlatform\Metadata\Exception\NotExposedHttpException - */ -class NotExposedHttpException extends NotFoundHttpException -{ -} diff --git a/src/Exception/OperationNotFoundException.php b/src/Exception/OperationNotFoundException.php deleted file mode 100644 index 855ba110b2e..00000000000 --- a/src/Exception/OperationNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Operation not found exception. - * - * @deprecated use ApiPlatform\Metadata\Exception\OperationNotFoundException - */ -class OperationNotFoundException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/Exception/PropertyNotFoundException.php b/src/Exception/PropertyNotFoundException.php deleted file mode 100644 index be1f406c97a..00000000000 --- a/src/Exception/PropertyNotFoundException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Property not found exception. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Exception\PropertyNotFoundException - */ -class PropertyNotFoundException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/ResourceClassNotFoundException.php b/src/Exception/ResourceClassNotFoundException.php deleted file mode 100644 index 408d7dd1b4c..00000000000 --- a/src/Exception/ResourceClassNotFoundException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Resource class not found exception. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException - */ -class ResourceClassNotFoundException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/ResourceClassNotSupportedException.php b/src/Exception/ResourceClassNotSupportedException.php deleted file mode 100644 index f8d0cc0fa6b..00000000000 --- a/src/Exception/ResourceClassNotSupportedException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Resource class not supported exception. - * - * @deprecated - * - * @author Kévin Dunglas - */ -class ResourceClassNotSupportedException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php deleted file mode 100644 index eab66a88207..00000000000 --- a/src/Exception/RuntimeException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Runtime exception. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Exception\RuntimeException - */ -class RuntimeException extends \RuntimeException implements ExceptionInterface -{ -} diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php deleted file mode 100644 index 2ab6032923a..00000000000 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\State\Pagination\ArrayPaginator; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - * - * @deprecated - */ -final class CollectionResolverFactory implements ResolverFactoryInterface -{ - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - // If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response. - if (null === $resourceClass || null === $rootClass || (null !== $source && !\array_key_exists($info->fieldName, $source))) { - return null; - } - - if (is_a($resourceClass, \BackedEnum::class, true) && $source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && !$operation->getProvider() && $source && \array_key_exists($info->fieldName, $source)) { - return ($this->serializeStage)(new ArrayPaginator($source[$info->fieldName], 0, \count($source[$info->fieldName])), $resourceClass, $operation, $resolverContext); - } - - $collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (!is_iterable($collection)) { - throw new \LogicException('Collection from read stage should be iterable.'); - } - - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryCollectionResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $collection = $queryResolver($collection, $resolverContext); - } - - // Only perform security stage on the top-level query - if (null === $source) { - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - 'previous_object' => $this->clone($collection), - ], - ]); - } - - return ($this->serializeStage)($collection, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php deleted file mode 100644 index 8e814e61a68..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ /dev/null @@ -1,117 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\Metadata\DeleteOperationInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function resolving a GraphQL mutation of an item. - * - * @author Alan Poulain - * @author Vincent Chalamon - * - * @deprecated - */ -final class ItemMutationResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly DeserializeStageInterface $deserializeStage, private readonly WriteStageInterface $writeStage, private readonly ValidateStageInterface $validateStage, private readonly ContainerInterface $mutationResolverLocator, private readonly SecurityPostValidationStageInterface $securityPostValidationStage) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - $previousItem = $this->clone($item); - - if ('delete' === $operation->getName() || $operation instanceof DeleteOperationInterface) { - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - $item = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - } - - $item = ($this->deserializeStage)($item, $resourceClass, $operation, $resolverContext); - - $mutationResolverId = $operation->getResolver(); - if (null !== $mutationResolverId) { - /** @var MutationResolverInterface $mutationResolver */ - $mutationResolver = $this->mutationResolverLocator->get($mutationResolverId); - $item = $mutationResolver($item, $resolverContext); - if (null !== $item && $resourceClass !== $itemClass = $this->getObjectClass($item)) { - throw new \LogicException(\sprintf('Custom mutation resolver "%s" has to return an item of class %s but returned an item of class %s.', $mutationResolverId, $operation->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - } - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - if (null !== $item) { - ($this->validateStage)($item, $resourceClass, $operation, $resolverContext); - - ($this->securityPostValidationStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - $persistResult = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - } - - return ($this->serializeStage)($persistResult ?? $item, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php deleted file mode 100644 index 394bcbeb1fe..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ /dev/null @@ -1,118 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving an item to resolve a GraphQL query. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - * - * @deprecated - */ -final class ItemResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { - // Data already fetched and normalized (field or nested resource) - if ($source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - if (!$operation) { - $operation = new Query(); - } - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - - $resourceClass = $operation->getOutput()['class'] ?? $resourceClass; - // The item retrieved can be of another type when using an identifier (see Relay Nodes at query.feature:23) - $resourceClass = $this->getResourceClass($item, $resourceClass); - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryItemResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $item = $queryResolver($item, $resolverContext); - $resourceClass = $this->getResourceClass($item, $resourceClass, \sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.'); - } - - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $this->clone($item), - ], - ]); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - }; - } - - /** - * @throws \UnexpectedValueException - */ - private function getResourceClass(?object $item, ?string $resourceClass, string $errorMessage = 'Resolver only handles items of class %s but retrieved item is of class %s.'): string - { - if (null === $item) { - if (null === $resourceClass) { - throw new \UnexpectedValueException('Resource class cannot be determined.'); - } - - return $resourceClass; - } - - $itemClass = $this->getObjectClass($item); - - if (null === $resourceClass) { - return $itemClass; - } - - if ($resourceClass !== $itemClass && !$item instanceof $resourceClass) { - throw new \UnexpectedValueException(\sprintf($errorMessage, (new \ReflectionClass($resourceClass))->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - - return $resourceClass; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php deleted file mode 100644 index 3fcfff6499d..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; - -/** - * Creates a function resolving a GraphQL subscription of an item. - * - * @author Alan Poulain - * - * @deprecated - */ -final class ItemSubscriptionResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SerializeStageInterface $serializeStage, private readonly SubscriptionManagerInterface $subscriptionManager, private readonly ?MercureSubscriptionIriGeneratorInterface $mercureSubscriptionIriGenerator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - - $result = ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - - $subscriptionId = $this->subscriptionManager->retrieveSubscriptionId($resolverContext, $result); - - if ($subscriptionId && ($mercure = $operation->getMercure())) { - if (!$this->mercureSubscriptionIriGenerator) { - throw new \LogicException('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - } - - $hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null; - $result['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub); - } - - return $result; - }; - } -} diff --git a/src/GraphQl/Resolver/Stage/DeserializeStage.php b/src/GraphQl/Resolver/Stage/DeserializeStage.php deleted file mode 100644 index 7cfcf08b7f8..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStage.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class DeserializeStage implements DeserializeStageInterface -{ - public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object - { - if (!($operation->canDeserialize() ?? true)) { - return $objectToPopulate; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - if (null !== $objectToPopulate) { - $denormalizationContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $objectToPopulate; - } - - $item = $this->denormalizer->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext); - - if (!\is_object($item)) { - throw new \UnexpectedValueException('Expected item to be an object.'); - } - - return $item; - } -} diff --git a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php b/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php deleted file mode 100644 index 146ec0aa143..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -interface DeserializeStageInterface -{ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php deleted file mode 100644 index 98a7a8a3a25..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStage.php +++ /dev/null @@ -1,192 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class ReadStage implements ReadStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ProviderInterface $provider, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null - { - if (!($operation->canRead() ?? true)) { - return $context['is_collection'] ? [] : null; - } - - $args = $context['args']; - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - if (!$context['is_collection']) { - $identifier = $this->getIdentifierFromContext($context); - $item = $this->getItem($identifier, $normalizationContext); - - if ($identifier && ($context['is_mutation'] || $context['is_subscription'])) { - if (null === $item) { - throw new NotFoundHttpException(\sprintf('Item "%s" not found.', $args['input']['id'])); - } - - if ($resourceClass !== $this->getObjectClass($item)) { - throw new \UnexpectedValueException(\sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $operation->getShortName())); - } - } - - return $item; - } - - if (null === $rootClass) { - return []; - } - - $uriVariables = []; - $normalizationContext['filters'] = $this->getNormalizedFilters($args); - $normalizationContext['operation'] = $operation; - - $source = $context['source']; - /** @var ResolveInfo $info */ - $info = $context['info']; - if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) { - $uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY]; - $normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]; - $normalizationContext['linkProperty'] = $info->fieldName; - } - - return $this->provider->provide($operation, $uriVariables, $normalizationContext); - } - - private function getItem(?string $identifier, array $normalizationContext): ?object - { - if (null === $identifier) { - return null; - } - - try { - $item = $this->iriConverter->getResourceFromIri($identifier, $normalizationContext); - } catch (ItemNotFoundException) { - return null; - } - - return $item; - } - - private function getNormalizedFilters(array $args): array - { - $filters = $args; - - foreach ($filters as $name => $value) { - if (\is_array($value)) { - if (strpos($name, '_list')) { - $name = substr($name, 0, \strlen($name) - \strlen('_list')); - } - - // If the value contains arrays, we need to merge them for the filters to understand this syntax, proper to GraphQL to preserve the order of the arguments. - if ($this->isSequentialArrayOfArrays($value)) { - $value = array_merge(...$value); - } - $filters[$name] = $this->getNormalizedFilters($value); - } - - if (\is_string($name) && strpos($name, $this->nestingSeparator)) { - // Gives a chance to relations/nested fields. - $index = array_search($name, array_keys($filters), true); - $filters = - \array_slice($filters, 0, $index + 1) + - [str_replace($this->nestingSeparator, '.', $name) => $value] + - \array_slice($filters, $index + 1); - } - } - - return $filters; - } - - public function isSequentialArrayOfArrays(array $array): bool - { - if (!$this->isSequentialArray($array)) { - return false; - } - - return $this->arrayContainsOnly($array, 'array'); - } - - public function isSequentialArray(array $array): bool - { - if ([] === $array) { - return false; - } - - return array_is_list($array); - } - - public function arrayContainsOnly(array $array, string $type): bool - { - return $array === array_filter($array, static fn ($item): bool => $type === \gettype($item)); - } - - /** - * Get class name of the given object. - */ - private function getObjectClass(object $object): string - { - return $this->getRealClassName($object::class); - } - - /** - * Get the real class name of a class name that could be a proxy. - */ - private function getRealClassName(string $className): string - { - // __CG__: Doctrine Common Marker for Proxy (ODM < 2.0 and ORM < 3.0) - // __PM__: Ocramius Proxy Manager (ODM >= 2.0) - $positionCg = strrpos($className, '\\__CG__\\'); - $positionPm = strrpos($className, '\\__PM__\\'); - - if (false === $positionCg && false === $positionPm) { - return $className; - } - - if (false !== $positionCg) { - return substr($className, $positionCg + 8); - } - - $className = ltrim($className, '\\'); - - return substr( - $className, - 8 + $positionPm, - strrpos($className, '\\') - ($positionPm + 8) - ); - } -} diff --git a/src/GraphQl/Resolver/Stage/ReadStageInterface.php b/src/GraphQl/Resolver/Stage/ReadStageInterface.php deleted file mode 100644 index 0c1670257ed..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStageInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -interface ReadStageInterface -{ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php deleted file mode 100644 index 8610ee781d1..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post denormalize stage of GraphQL resolvers. - * - * @author Vincent Chalamon - * - * @deprecated - */ -final class SecurityPostDenormalizeStage implements SecurityPostDenormalizeStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostDenormalize(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostDenormalizeMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php deleted file mode 100644 index 5a9bfae3b19..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post deserialization stage of GraphQL resolvers. - * - * @author Vincent Chalamon - * - * @deprecated - */ -interface SecurityPostDenormalizeStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php deleted file mode 100644 index 4ab18fa8716..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @deprecated use providers instead of stages - * - * @author Vincent Chalamon - * @author Grégoire Pineau - */ -final class SecurityPostValidationStage implements SecurityPostValidationStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostValidation(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostValidationMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php deleted file mode 100644 index 2e23c0393c0..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @author Vincent Chalamon - * @author Grégoire Pineau - * - * @deprecated - */ -interface SecurityPostValidationStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStage.php b/src/GraphQl/Resolver/Stage/SecurityStage.php deleted file mode 100644 index e1db9550826..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStage.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class SecurityStage implements SecurityStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurity(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityStageInterface.php deleted file mode 100644 index 3ee436365bc..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - * @author Vincent Chalamon - * - * @deprecated - */ -interface SecurityStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php deleted file mode 100644 index b0ab622bbb2..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStage.php +++ /dev/null @@ -1,246 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class SerializeStage implements SerializeStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly NormalizerInterface $normalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly Pagination $pagination) - { - } - - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array - { - $isCollection = $operation instanceof CollectionOperationInterface; - $isMutation = $operation instanceof Mutation; - $isSubscription = $operation instanceof Subscription; - $shortName = $operation->getShortName(); - $operationName = $operation->getName(); - - if (!($operation->canSerialize() ?? true)) { - if ($isCollection) { - if ($this->pagination->isGraphQlEnabled($operation, $context)) { - return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->getDefaultCursorBasedPaginatedData() : - $this->getDefaultPageBasedPaginatedData(); - } - - return []; - } - - if ($isMutation) { - return $this->getDefaultMutationData($context); - } - - if ($isSubscription) { - return $this->getDefaultSubscriptionData($context); - } - - return null; - } - - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - $data = null; - if (!$isCollection) { - if ($isMutation && 'delete' === $operationName) { - $data = ['id' => $this->getIdentifierFromContext($context)]; - } else { - $data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext); - } - } - - if ($isCollection && is_iterable($itemOrCollection)) { - if (!$this->pagination->isGraphQlEnabled($operation, $context)) { - $data = []; - foreach ($itemOrCollection as $index => $object) { - $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - } else { - $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) : - $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context); - } - } - - if (null !== $data && !\is_array($data)) { - throw new \UnexpectedValueException('Expected serialized data to be a nullable array.'); - } - - if ($isMutation || $isSubscription) { - $wrapFieldName = lcfirst($shortName); - - return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context)); - } - - return $data; - } - - /** - * @throws \LogicException - * @throws \UnexpectedValueException - */ - private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $args = $context['args']; - - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class)); - } - - $selection = $context['info']->getFieldSelection(1); - - $offset = 0; - $totalItems = 1; // For partial pagination, always consider there is at least one item. - $data = ['edges' => []]; - if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) { - $nbPageItems = $collection->count(); - if (isset($args['after'])) { - $after = base64_decode($args['after'], true); - if (false === $after || '' === $args['after']) { - throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['after'])); - } - $offset = 1 + (int) $after; - } - - if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) { - $totalItems = $collection->getTotalItems(); - if (isset($args['before'])) { - $before = base64_decode($args['before'], true); - if (false === $before || '' === $args['before']) { - throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['before'])); - } - $offset = (int) $before - $nbPageItems; - } - if (isset($args['last']) && !isset($args['before'])) { - $offset = $totalItems - $args['last']; - } - } - - $offset = max(0, $offset); - - $data = $this->getDefaultCursorBasedPaginatedData(); - if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) { - isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset); - $end = $offset + $nbPageItems - 1; - isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0)); - isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0; - if ($collection instanceof PaginatorInterface) { - isset($selection['totalCount']) && $data['totalCount'] = $totalItems; - - $itemsPerPage = $collection->getItemsPerPage(); - isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; - } - } - } - - $index = 0; - foreach ($collection as $object) { - $edge = [ - 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), - ]; - if (isset($selection['edges']['cursor'])) { - $edge['cursor'] = base64_encode((string) ($index + $offset)); - } - $data['edges'][$index] = $edge; - ++$index; - } - - return $data; - } - - /** - * @throws \LogicException - */ - private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $data = ['collection' => []]; - - $selection = $context['info']->getFieldSelection(1); - if (isset($selection['paginationInfo'])) { - $data['paginationInfo'] = []; - if (isset($selection['paginationInfo']['itemsPerPage'])) { - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class)); - } - $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); - } - if (isset($selection['paginationInfo']['totalCount'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class)); - } - $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); - } - if (isset($selection['paginationInfo']['lastPage'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); - } - $data['paginationInfo']['lastPage'] = $collection->getLastPage(); - } - if (isset($selection['paginationInfo']['hasNextPage'])) { - if (!($collection instanceof HasNextPagePaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return hasNextPage field.', HasNextPagePaginatorInterface::class)); - } - $data['paginationInfo']['hasNextPage'] = $collection->hasNextPage(); - } - } - - foreach ($collection as $object) { - $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - - return $data; - } - - private function getDefaultCursorBasedPaginatedData(): array - { - return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]; - } - - private function getDefaultPageBasedPaginatedData(): array - { - return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0., 'hasNextPage' => false]]; - } - - private function getDefaultMutationData(array $context): array - { - return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null]; - } - - private function getDefaultSubscriptionData(array $context): array - { - return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null]; - } -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php b/src/GraphQl/Resolver/Stage/SerializeStageInterface.php deleted file mode 100644 index eda72060f80..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -interface SerializeStageInterface -{ - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array; -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStage.php b/src/GraphQl/Resolver/Stage/ValidateStage.php deleted file mode 100644 index 54a67f29b1b..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStage.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Validator\ValidatorInterface; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class ValidateStage implements ValidateStageInterface -{ - public function __construct(private readonly ValidatorInterface $validator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void - { - if (!($operation->canValidate() ?? true)) { - return; - } - - $this->validator->validate($object, $operation->getValidationContext() ?? []); - } -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php b/src/GraphQl/Resolver/Stage/ValidateStageInterface.php deleted file mode 100644 index 8d349bf8292..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -interface ValidateStageInterface -{ - /** - * @throws Error - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/WriteStage.php b/src/GraphQl/Resolver/Stage/WriteStage.php deleted file mode 100644 index fb430005309..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStage.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\State\ProcessorInterface; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -final class WriteStage implements WriteStageInterface -{ - public function __construct(private readonly ProcessorInterface $processor, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object - { - if (null === $data || !($operation->canWrite() ?? true)) { - return $data; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - - return $this->processor->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext); - } -} diff --git a/src/GraphQl/Resolver/Stage/WriteStageInterface.php b/src/GraphQl/Resolver/Stage/WriteStageInterface.php deleted file mode 100644 index b02501ed80a..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStageInterface.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - * - * @deprecated - */ -interface WriteStageInterface -{ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php index 5b440aa13ac..df9f096d596 100644 --- a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php @@ -14,7 +14,6 @@ namespace ApiPlatform\GraphQl\Serializer\Exception; use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; @@ -39,7 +38,7 @@ public function __construct(private readonly array $exceptionToStatus = []) public function normalize(mixed $object, ?string $format = null, array $context = []): array { $validationException = $object->getPrevious(); - if (!($validationException instanceof ConstraintViolationListAwareExceptionInterface || $validationException instanceof LegacyConstraintViolationListAwareExceptionInterface)) { + if (!$validationException instanceof ConstraintViolationListAwareExceptionInterface) { throw new RuntimeException(\sprintf('Object is not a "%s".', ConstraintViolationListAwareExceptionInterface::class)); } diff --git a/src/GraphQl/Serializer/ObjectNormalizer.php b/src/GraphQl/Serializer/ObjectNormalizer.php index 03f0272fb77..18f2716eebd 100644 --- a/src/GraphQl/Serializer/ObjectNormalizer.php +++ b/src/GraphQl/Serializer/ObjectNormalizer.php @@ -13,22 +13,17 @@ namespace ApiPlatform\GraphQl\Serializer; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with GraphQL metadata when appropriate, but otherwise just * passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use ClassInfoTrait; @@ -36,7 +31,7 @@ final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMe public const ITEM_RESOURCE_CLASS_KEY = '#itemResourceClass'; public const ITEM_IDENTIFIERS_KEY = '#itemIdentifiers'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface $identifiersExtractor) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface $identifiersExtractor) { } @@ -50,33 +45,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} * diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index fe3694a5077..8e2532aa33e 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -13,8 +13,6 @@ namespace ApiPlatform\GraphQl\Subscription; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStage; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Subscription; @@ -38,11 +36,8 @@ final class SubscriptionManager implements OperationAwareSubscriptionManagerInte use ResourceClassInfoTrait; use SortTrait; - public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly SerializeStageInterface|ProcessorInterface|null $serializeStage = null, private readonly ?IriConverterInterface $iriConverter = null, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) + public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ProcessorInterface $normalizeProcessor, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { - if (!$serializeStage instanceof ProcessorInterface) { - trigger_deprecation('api-platform/core', '4.0', \sprintf('Using an instanceof "%s" is deprecated, use "%s" instead.', SerializeStageInterface::class, ProcessorInterface::class)); - } } public function retrieveSubscriptionId(array $context, ?array $result, ?Operation $operation = null): ?string @@ -89,13 +84,7 @@ public function getPushPayloads(object $object): array $resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; /** @var Operation */ $operation = (new Subscription())->withName('update_subscription')->withShortName($shortName); - if ($this->serializeStage instanceof ProcessorInterface) { - $data = $this->serializeStage->process($object, $operation, [], $resolverContext); - } elseif ($this->serializeStage instanceof SerializeStage) { - $data = ($this->serializeStage)($object, $resourceClass, $operation, $resolverContext); - } else { - throw new \LogicException(); - } + $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); unset($data['clientSubscriptionId']); diff --git a/src/GraphQl/Tests/Action/EntrypointActionTest.php b/src/GraphQl/Tests/Action/EntrypointActionTest.php index 14a4d460deb..cf6ada1cc8b 100644 --- a/src/GraphQl/Tests/Action/EntrypointActionTest.php +++ b/src/GraphQl/Tests/Action/EntrypointActionTest.php @@ -92,9 +92,7 @@ public function testPostJsonAction(): void $this->assertEqualsWithoutDateHeader(new JsonResponse(['GraphQL']), $mockedEntrypoint($request)); } - /** - * @dataProvider multipartRequestProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('multipartRequestProvider')] public function testMultipartRequestAction(?string $operations, ?string $map, array $files, array $variables, Response $expectedResponse): void { $requestParams = []; diff --git a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php deleted file mode 100644 index 2bb08db8033..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php +++ /dev/null @@ -1,269 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\CollectionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class CollectionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private CollectionResolverFactory $collectionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->collectionResolverFactory = new CollectionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal(), - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['testField' => 0]; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveEnumFieldFromSource(): void - { - $resourceClass = GenderTypeEnum::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['genders' => [GenderTypeEnum::MALE, GenderTypeEnum::FEMALE]]; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'genders'; - - $this->assertSame([GenderTypeEnum::MALE, GenderTypeEnum::FEMALE], ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveFieldNotInSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - // Null should be returned if the field isn't in the source - as its lack of presence will be due to @ApiProperty security stripping unauthorized fields - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullRootClass(): void - { - $resourceClass = \stdClass::class; - $rootClass = null; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveBadReadStageCollection(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Collection from read stage should be iterable.'); - - ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withResolver('query_resolver_id')->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $customCollection = [new \stdClass()]; - $customCollection[0]->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): array => $customCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - 'previous_object' => $customCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php deleted file mode 100644 index c7220499409..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php +++ /dev/null @@ -1,321 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemMutationResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\Metadata\GraphQl\Mutation; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - */ -class ItemMutationResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemMutationResolverFactory $itemMutationResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $deserializeStageProphecy; - private ObjectProphecy $writeStageProphecy; - private ObjectProphecy $validateStageProphecy; - private ObjectProphecy $mutationResolverLocatorProphecy; - private ObjectProphecy $securityPostValidationStageProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->deserializeStageProphecy = $this->prophesize(DeserializeStageInterface::class); - $this->writeStageProphecy = $this->prophesize(WriteStageInterface::class); - $this->validateStageProphecy = $this->prophesize(ValidateStageInterface::class); - $this->mutationResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - $this->securityPostValidationStageProphecy = $this->prophesize(SecurityPostValidationStageInterface::class); - - $this->itemMutationResolverFactory = new ItemMutationResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->deserializeStageProphecy->reveal(), - $this->writeStageProphecy->reveal(), - $this->validateStageProphecy->reveal(), - $this->mutationResolverLocatorProphecy->reveal(), - $this->securityPostValidationStageProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operation = (new Mutation())->withName('create'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperation(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNullDeserializeStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = null; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->writeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $serializeStageData = null; - $this->serializeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveDelete(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'delete'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->deserializeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName)->withShortName('shortName'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new Dummy(); - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Custom mutation resolver "query_resolver_id" has to return an item of class shortName but returned an item of class Dummy.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php deleted file mode 100644 index 7ab86f63f5f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php +++ /dev/null @@ -1,268 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo; -use ApiPlatform\Metadata\GraphQl\Query; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class ItemResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemResolverFactory $itemResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->itemResolverFactory = new ItemResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal() - ); - } - - /** - * @dataProvider itemResourceProvider - */ - public function testResolve(?string $resourceClass, string $determinedResourceClass, ?object $readStageItem): void - { - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $determinedResourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public static function itemResourceProvider(): array - { - return [ - 'nominal' => [\stdClass::class, \stdClass::class, new \stdClass()], - 'null item' => [\stdClass::class, \stdClass::class, null], - 'null resource class' => [null, \stdClass::class, new \stdClass()], - ]; - } - - public function testResolveNested(): void - { - $source = ['nested' => ['already_serialized']]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nested'; - - $this->assertEquals(['already_serialized'], ($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveNestedNullValue(): void - { - $source = ['nestedNullValue' => null]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nestedNullValue'; - - $this->assertNull(($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoResourceNoItem(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = null; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resource class cannot be determined.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveBadItem(): void - { - $resourceClass = Dummy::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resolver only handles items of class Dummy but retrieved item is of class stdClass.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $customItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new Dummy(); - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Custom query resolver "query_resolver_id" has to return an item of class stdClass but returned an item of class Dummy.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveInheritedClass(): void - { - $resourceClass = ParentFoo::class; - $rootClass = $resourceClass; - $operationName = 'custom_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new ChildFoo(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php deleted file mode 100644 index 5d455a6df0f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class ItemSubscriptionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemSubscriptionResolverFactory $itemSubscriptionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $subscriptionManagerProphecy; - private ObjectProphecy $mercureSubscriptionIriGeneratorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->subscriptionManagerProphecy = $this->prophesize(SubscriptionManagerInterface::class); - $this->mercureSubscriptionIriGeneratorProphecy = $this->prophesize(MercureSubscriptionIriGeneratorInterface::class); - - $this->itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - $this->mercureSubscriptionIriGeneratorProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withMercure(true)->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->shouldBeCalled()->willReturn($subscriptionId); - - $mercureUrl = 'mercure-url'; - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl($subscriptionId, null)->shouldBeCalled()->willReturn($mercureUrl); - - $this->assertSame($serializeStageData + ['mercureUrl' => $mercureUrl], ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperationName(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoSubscriptionId(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn(null); - - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl(Argument::any())->shouldNotBeCalled(); - - $this->assertSame($serializeStageData, ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNoMercureSubscriptionIriGenerator(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - /** @var Operation $operation */ - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn($subscriptionId); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - - $itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - null - ); - - ($itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php index ee0be4aef60..66278039554 100644 --- a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php +++ b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php @@ -26,9 +26,7 @@ class ResolverFactoryTest extends TestCase { - /** - * @dataProvider graphQlQueries - */ + #[\PHPUnit\Framework\Attributes\DataProvider('graphQlQueries')] public function testGraphQlResolver(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?Operation $providedOperation = null, ?Operation $processedOperation = null): void { $returnValue = new \stdClass(); diff --git a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php deleted file mode 100644 index 174dbe06f92..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Alan Poulain - */ -class DeserializeStageTest extends TestCase -{ - use ProphecyTrait; - - private DeserializeStage $deserializeStage; - private ObjectProphecy $denormalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->denormalizerProphecy = $this->prophesize(DenormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->deserializeStage = new DeserializeStage( - $this->denormalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApplyDisabled(?object $objectToPopulate): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withClass($resourceClass)->withDeserialize(false); - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, []); - - $this->assertSame($objectToPopulate, $result); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApply(?object $objectToPopulate, array $denormalizationContext): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - $context = ['args' => ['input' => 'myInput']]; - - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->shouldBeCalled()->willReturn($denormalizationContext); - - $denormalizedData = new \stdClass(); - $this->denormalizerProphecy->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext)->shouldBeCalled()->willReturn($denormalizedData); - - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, $context); - - $this->assertSame($denormalizedData, $result); - } - - public static function objectToPopulateProvider(): array - { - return [ - 'null' => [null, ['denormalization' => true]], - 'object' => [$object = new \stdClass(), ['denormalization' => true, ItemNormalizer::OBJECT_TO_POPULATE => $object]], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php deleted file mode 100644 index 40c6c37a03f..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php +++ /dev/null @@ -1,270 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * @author Alan Poulain - */ -class ReadStageTest extends TestCase -{ - use ProphecyTrait; - - private ReadStage $readStage; - private ObjectProphecy $iriConverterProphecy; - private ObjectProphecy $providerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $this->providerProphecy = $this->prophesize(ProviderInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->readStage = new ReadStage( - $this->iriConverterProphecy->reveal(), - $this->providerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - '_' - ); - } - - /** - * @dataProvider contextProvider - */ - public function testApplyDisabled(array $context, object|array|null $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withRead(false)->withName('item_query')->withClass($resourceClass); - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function contextProvider(): array - { - return [ - 'item context' => [['is_collection' => false], null], - 'collection context' => [['is_collection' => true], []], - ]; - } - - /** - * @dataProvider itemProvider - */ - public function testApplyItem(?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => ['id' => $identifier], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [null, $item, false, null], - 'identifier' => ['identifier', $item, false, $item], - 'identifier not found' => ['identifier_not_found', $item, true, null], - ]; - } - - /** - * @dataProvider itemMutationOrSubscriptionProvider - */ - public function testApplyMutationOrSubscription(bool $isMutation, bool $isSubscription, string $resourceClass, ?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'create'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => $isMutation, - 'is_subscription' => $isSubscription, - 'args' => ['input' => ['id' => $identifier]], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName)->withShortName('shortName'); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemMutationOrSubscriptionProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [true, false, 'myResource', null, $item, false, null], - 'identifier' => [true, false, \stdClass::class, 'identifier', $item, false, $item], - 'identifier bad item' => [true, false, 'myResource', 'identifier', $item, false, $item, \UnexpectedValueException::class, 'Item "identifier" did not match expected type "shortName".'], - 'identifier not found' => [true, false, 'myResource', 'identifier_not_found', $item, true, null, NotFoundHttpException::class, 'Item "identifier_not_found" not found.'], - 'no identifier (subscription)' => [false, true, 'myResource', null, $item, false, null], - 'identifier (subscription)' => [false, true, \stdClass::class, 'identifier', $item, false, $item], - ]; - } - - /** - * @dataProvider collectionProvider - */ - public function testApplyCollection(array $args, ?string $rootClass, ?array $source, array $expectedFilters, iterable $expectedResult): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $info, - 'source' => $source, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - $normalizationContext = ['normalization' => true, 'operation' => $operation]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->providerProphecy->provide($operation, [], $normalizationContext + ['filters' => $expectedFilters])->willReturn([]); - $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource', 'linkProperty' => 'resource'])->willReturn(['resource']); - - $result = ($this->readStage)($resourceClass, $rootClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public function testPreserveOrderOfOrderFiltersIfNested(): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [ - 'order' => [ - 'some_field' => 'ASC', - 'localField' => 'ASC', - ], - ], - 'info' => $info, - 'source' => null, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - ($this->readStage)($resourceClass, $resourceClass, $operation, $context); - - $this->providerProphecy->provide($operation, [], Argument::that(fn ($args): bool => // Prophecy does not check the order of items in associative arrays. Checking if some.field comes first manually -array_search('some.field', array_keys($args['filters']['order']), true) < - array_search('localField', array_keys($args['filters']['order']), true)))->shouldHaveBeenCalled(); - } - - public static function collectionProvider(): array - { - return [ - 'no root class' => [[], null, null, [], []], - 'nominal' => [ - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2']], - 'myResource', - null, - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2'], 'filter.list' => 'filtered', 'filter_field' => ['filtered1', 'filtered2'], 'filter.field' => ['filtered1', 'filtered2']], - [], - ], - 'with array filter syntax' => [ - ['filter' => [['filterArg1' => 'filterValue1'], ['filterArg2' => 'filterValue2']]], - 'myResource', - null, - ['filter' => ['filterArg1' => 'filterValue1', 'filterArg2' => 'filterValue2']], - [], - ], - 'with resource' => [ - [], - 'myResource', - ['resource' => [], ItemNormalizer::ITEM_IDENTIFIERS_KEY => ['id' => 3], ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => 'myResource'], - [], - ['resource'], - ], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php deleted file mode 100644 index 1b870ea2b57..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostDenormalizeStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostDenormalizeStage $securityPostDenormalizeStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $resourceClass = 'myResource'; - $operation = new Query(); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->expectException(\LogicException::class); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php deleted file mode 100644 index d824578b9d9..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostValidationStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostValidationStage $securityPostValidationStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostValidationStage = new SecurityPostValidationStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostValidationStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostValidationStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php deleted file mode 100644 index 1eb69316e4e..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityStage $securityStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityStage = new SecurityStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityStage = new SecurityStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityStage = new SecurityStage(null); - - $resourceClass = 'myResource'; - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, new Query(), []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php deleted file mode 100644 index afcf85ffbef..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php +++ /dev/null @@ -1,287 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\Pagination\ArrayPaginator; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * @author Alan Poulain - */ -class SerializeStageTest extends TestCase -{ - use ProphecyTrait; - - private ObjectProphecy $normalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - private ObjectProphecy $resolveInfoProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->normalizerProphecy = $this->prophesize(NormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); - } - - /** - * @dataProvider applyDisabledProvider - */ - public function testApplyDisabled(Operation $operation, bool $paginationEnabled, ?array $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = $operation->withSerialize(false); - - $result = ($this->createSerializeStage($paginationEnabled))(null, $resourceClass, $operation, []); - - $this->assertSame($expectedResult, $result); - } - - public static function applyDisabledProvider(): array - { - return [ - 'item' => [new Query(), false, null], - 'collection with pagination' => [new QueryCollection(), true, ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]], - 'collection without pagination' => [new QueryCollection(), false, []], - 'mutation' => [new Mutation(), false, ['clientMutationId' => null]], - 'subscription' => [new Subscription(), false, ['clientSubscriptionId' => null]], - ]; - } - - /** - * @dataProvider applyProvider - */ - public function testApply(object|array $itemOrCollection, string $operationName, callable $contextFactory, bool $paginationEnabled, ?array $expectedResult): void - { - $context = $contextFactory($this); - - $resourceClass = 'myResource'; - $operation = $context['is_mutation'] ? new Mutation() : new Query(); - if ($context['is_subscription']) { - $operation = new Subscription(); - } - - if ($context['is_collection'] ?? false) { - $operation = new QueryCollection(); - } - - /** @var Operation $operation */ - $operation = $operation->withShortName('shortName')->withName($operationName)->withClass($resourceClass); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - $result = ($this->createSerializeStage($paginationEnabled))($itemOrCollection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyProvider(): iterable - { - $defaultContextFactory = fn (self $that): array => [ - 'args' => [], - 'info' => $that->resolveInfoProphecy->reveal(), - ]; - - yield 'item' => [new \stdClass(), 'item_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false], false, ['normalized_item']]; - yield 'collection without pagination' => [[new \stdClass(), new \stdClass()], 'collection_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => true, 'is_mutation' => false, 'is_subscription' => false], false, [['normalized_item'], ['normalized_item']]]; - yield 'mutation' => [new \stdClass(), 'create', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientMutationId' => 'clientMutationId']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['normalized_item'], 'clientMutationId' => 'clientMutationId']]; - yield 'delete mutation' => [new \stdClass(), 'delete', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['id' => '/iri/4']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['id' => '/iri/4'], 'clientMutationId' => null]]; - yield 'subscription' => [new \stdClass(), 'update', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientSubscriptionId' => 'clientSubscriptionId']], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]), false, ['shortName' => ['normalized_item'], 'clientSubscriptionId' => 'clientSubscriptionId']]; - } - - /** - * @dataProvider applyCollectionWithPaginationProvider - */ - public function testApplyCollectionWithPagination(iterable|callable $collection, array $args, ?array $expectedResult, bool $pageBasedPagination, array $getFieldSelection = [], ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->createSerializeStage(true))(\is_callable($collection) ? $collection($this) : $collection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyCollectionWithPaginationProvider(): iterable - { - $partialPaginatorFactory = function (self $that): PartialPaginatorInterface { - $partialPaginatorProphecy = $that->prophesize(PartialPaginatorInterface::class); - $partialPaginatorProphecy->count()->willReturn(2); - $partialPaginatorProphecy->valid()->willReturn(false); - $partialPaginatorProphecy->getItemsPerPage()->willReturn(2.0); - $partialPaginatorProphecy->rewind(); - - return $partialPaginatorProphecy->reveal(); - }; - - yield 'cursor - not paginator' => [[], [], null, false, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; - yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => [], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with after cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), ['after' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), ['after' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with before cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 1), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), ['before' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), ['before' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with last' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator' => [$partialPaginatorFactory, [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - - yield 'page - not paginator, itemsPerPage requested' => [[], [], null, true, ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; - yield 'page - not paginator, lastPage requested' => [[], [], null, true, ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; - yield 'page - not paginator, totalCount requested' => [[], [], null, true, ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; - yield 'page - not paginator, hasNextPage requested' => [[], [], null, true, ['paginationInfo' => ['hasNextPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\HasNextPagePaginatorInterface to return hasNextPage field.']; - yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - empty paginator - hasNextPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator page 1 - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['hasNextPage' => true]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator with page - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - } - - /** - * @dataProvider applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider - */ - public function testApplyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequested(bool $pageBasedPagination, array $getFieldSelection = [], bool $getTotalItemsCalled = false): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [], - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - $collectionProphecy = $this->prophesize(PaginatorInterface::class); - $collectionProphecy->getTotalItems()->willReturn(1); - $collectionProphecy->count()->willReturn(1); - $collectionProphecy->getItemsPerPage()->willReturn(20.0); - $collectionProphecy->valid()->willReturn(false); - $collectionProphecy->rewind(); - if ($getTotalItemsCalled) { - $collectionProphecy->getTotalItems()->shouldBeCalledOnce(); - } else { - $collectionProphecy->getTotalItems()->shouldNotBeCalled(); - } - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - ($this->createSerializeStage(true))($collectionProphecy->reveal(), $resourceClass, $operation, $context); - } - - public static function applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider(): iterable - { - yield 'cursor - totalCount requested' => [false, ['totalCount' => true], true]; - yield 'cursor - totalCount not requested' => [false, [], false]; - yield 'page - totalCount requested' => [true, ['paginationInfo' => ['totalCount' => true]], true]; - yield 'page - totalCount not requested' => [true, [], false]; - } - - public function testApplyBadNormalizedData(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $context = ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false, 'args' => [], 'info' => $this->prophesize(ResolveInfo::class)->reveal()]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(0); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Expected serialized data to be a nullable array.'); - - ($this->createSerializeStage(false))(new \stdClass(), $resourceClass, $operation, $context); - } - - private function createSerializeStage(bool $paginationEnabled): SerializeStage - { - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('')); - $pagination = new Pagination([], ['enabled' => $paginationEnabled]); - - return new SerializeStage( - $this->normalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - $pagination - ); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php deleted file mode 100644 index 41c683f3d8c..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Validator\Exception\ValidationException; -use ApiPlatform\Validator\ValidatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Validator\ConstraintViolationList; - -/** - * @author Alan Poulain - */ -class ValidateStageTest extends TestCase -{ - use ProphecyTrait; - - private ValidateStage $validateStage; - private ObjectProphecy $validatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->validatorProphecy = $this->prophesize(ValidatorInterface::class); - - $this->validateStage = new ValidateStage( - $this->validatorProphecy->reveal() - ); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withValidate(false)->withName('item_query'); - - $this->validatorProphecy->validate(Argument::cetera())->shouldNotBeCalled(); - - ($this->validateStage)(new \stdClass(), $resourceClass, $operation, []); - } - - public function testApply(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withValidationContext(['groups' => $validationGroups]); - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled(); - - ($this->validateStage)($object, $resourceClass, $operation, []); - } - - public function testApplyNotValidated(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withValidationContext(['groups' => $validationGroups])->withName('item_query'); - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = ['info' => $info]; - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled()->willThrow(new ValidationException(new ConstraintViolationList())); - - $this->expectException(ValidationException::class); - - ($this->validateStage)($object, $resourceClass, $operation, $context); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php b/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php deleted file mode 100644 index 25544062faf..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\WriteStage; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\State\ProcessorInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class WriteStageTest extends TestCase -{ - use ProphecyTrait; - - private WriteStage $writeStage; - private ObjectProphecy $processorProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->processorProphecy = $this->prophesize(ProcessorInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->writeStage = new WriteStage( - $this->processorProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - public function testNoData(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query'); - - $result = ($this->writeStage)(null, $resourceClass, $operation, []); - - $this->assertNull($result); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withWrite(false); - - $data = new \stdClass(); - $result = ($this->writeStage)($data, $resourceClass, $operation, []); - - $this->assertSame($data, $result); - } - - public function testApply(): void - { - $operationName = 'create'; - $resourceClass = 'myResource'; - $context = []; - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName); - - $denormalizationContext = ['denormalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->willReturn($denormalizationContext); - - $data = new \stdClass(); - $processedData = new \stdClass(); - $this->processorProphecy->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext)->shouldBeCalled()->willReturn($processedData); - - $result = ($this->writeStage)($data, $resourceClass, $operation, $context); - - $this->assertSame($processedData, $result); - } -} diff --git a/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php b/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php index 9a66078a65e..2c54b9c4235 100644 --- a/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php +++ b/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php @@ -35,9 +35,7 @@ protected function setUp(): void $this->httpExceptionNormalizer = new HttpExceptionNormalizer(); } - /** - * @dataProvider exceptionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')] public function testNormalize(HttpException $exception, string $expectedExceptionMessage, int $expectedStatus, string $expectedCategory): void { $error = new Error('test message', null, null, [], null, $exception); diff --git a/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php b/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php index 847169ef871..f6a913b4670 100644 --- a/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php +++ b/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php @@ -82,9 +82,7 @@ private function buildOperationFromContext(bool $isMutation, bool $isSubscriptio return $operation; } - /** - * @dataProvider createNormalizationContextProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createNormalizationContextProvider')] public function testCreateNormalizationContext(?string $resourceClass, string $operationName, array $fields, bool $isMutation, bool $isSubscription, bool $noInfo, array $expectedContext, ?callable $advancedNameConverter = null, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void { $resolverContext = []; @@ -296,9 +294,7 @@ public static function createNormalizationContextProvider(): iterable ]; } - /** - * @dataProvider createDenormalizationContextProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createDenormalizationContextProvider')] public function testCreateDenormalizationContext(?string $resourceClass, string $operationName, array $expectedContext): void { $operation = $this->buildOperationFromContext(true, false, $expectedContext, false, $resourceClass); diff --git a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php index e34c8a3b743..6213938e7dd 100644 --- a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php +++ b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php @@ -42,9 +42,7 @@ protected function setUp(): void $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); } - /** - * @dataProvider processItems - */ + #[\PHPUnit\Framework\Attributes\DataProvider('processItems')] public function testProcess($body, $operation): void { $context = ['args' => []]; @@ -66,9 +64,7 @@ public static function processItems(): array ]; } - /** - * @dataProvider processCollection - */ + #[\PHPUnit\Framework\Attributes\DataProvider('processCollection')] public function testProcessCollection($collection, $operation, $args, ?array $expectedResult, array $getFieldSelection, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void { $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); diff --git a/src/GraphQl/Tests/Type/FieldsBuilderTest.php b/src/GraphQl/Tests/Type/FieldsBuilderTest.php index 8b24844a0ca..489f78086f2 100644 --- a/src/GraphQl/Tests/Type/FieldsBuilderTest.php +++ b/src/GraphQl/Tests/Type/FieldsBuilderTest.php @@ -63,9 +63,6 @@ class FieldsBuilderTest extends TestCase private ObjectProphecy $typeBuilderProphecy; private ObjectProphecy $typeConverterProphecy; private ObjectProphecy $itemResolverFactoryProphecy; - private ObjectProphecy $collectionResolverFactoryProphecy; - private ObjectProphecy $itemMutationResolverFactoryProphecy; - private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; private ObjectProphecy $filterLocatorProphecy; private ObjectProphecy $resourceClassResolverProphecy; private FieldsBuilder $fieldsBuilder; @@ -82,9 +79,6 @@ protected function setUp(): void $this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); $this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->itemMutationResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); @@ -92,7 +86,7 @@ protected function setUp(): void private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -119,16 +113,14 @@ public function testGetNodeQueryFields(): void $this->assertSame($itemResolver, $nodeQueryFields['resolve']); } - /** - * @dataProvider itemQueryFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('itemQueryFieldsProvider')] public function testGetItemQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -200,9 +192,7 @@ public static function itemQueryFieldsProvider(): array ]; } - /** - * @dataProvider collectionQueryFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('collectionQueryFieldsProvider')] public function testGetCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); @@ -210,7 +200,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getPaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); - $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); $filterProphecy->getDescription($resourceClass)->willReturn([ @@ -353,16 +343,14 @@ public static function collectionQueryFieldsProvider(): array ]; } - /** - * @dataProvider mutationFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('mutationFieldsProvider')] public function testGetMutationFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $mutationResolver, array $expectedMutationFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -415,9 +403,7 @@ public static function mutationFieldsProvider(): array ]; } - /** - * @dataProvider subscriptionFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('subscriptionFieldsProvider')] public function testGetSubscriptionFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $subscriptionResolver, array $expectedSubscriptionFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); @@ -425,7 +411,7 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); - $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($subscriptionResolver); $subscriptionFields = $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation); @@ -480,9 +466,7 @@ public static function subscriptionFieldsProvider(): array ]; } - /** - * @dataProvider resourceObjectTypeFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resourceObjectTypeFieldsProvider')] public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?callable $advancedNameConverterFactory = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); @@ -499,14 +483,14 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); - $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { + $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation, Argument::any())->willReturn(static function (): void { }); } if ('propertyNestedResource' === $propertyName) { $nestedResourceQueryOperation = new Query(); $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation, Argument::any())->willReturn(static function (): void { }); } } @@ -869,9 +853,7 @@ public function testGetEnumFields(): void ], $enumFields); } - /** - * @dataProvider resolveResourceArgsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resolveResourceArgsProvider')] public function testResolveResourceArgs(array $args, array $expectedResolvedArgs, ?string $expectedExceptionMessage = null): void { if (null !== $expectedExceptionMessage) { diff --git a/src/GraphQl/Tests/Type/SchemaBuilderTest.php b/src/GraphQl/Tests/Type/SchemaBuilderTest.php index 8428663f50f..13ef9fa04b1 100644 --- a/src/GraphQl/Tests/Type/SchemaBuilderTest.php +++ b/src/GraphQl/Tests/Type/SchemaBuilderTest.php @@ -62,9 +62,7 @@ protected function setUp(): void $this->schemaBuilder = new SchemaBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->typesFactoryProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->fieldsBuilderProphecy->reveal()); } - /** - * @dataProvider schemaProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('schemaProvider')] public function testGetSchema(string $resourceClass, ResourceMetadataCollection $resourceMetadata, ObjectType $expectedQueryType, ?ObjectType $expectedMutationType, ?ObjectType $expectedSubscriptionType): void { $type = new StringType(['name' => 'MyType']); diff --git a/src/GraphQl/Tests/Type/TypeBuilderTest.php b/src/GraphQl/Tests/Type/TypeBuilderTest.php index 11a5f2ae7d6..737ee37ce56 100644 --- a/src/GraphQl/Tests/Type/TypeBuilderTest.php +++ b/src/GraphQl/Tests/Type/TypeBuilderTest.php @@ -125,9 +125,7 @@ public function testGetResourceObjectTypeOutputClass(): void $resourceObjectType->config['fields'](); } - /** - * @dataProvider resourceObjectTypeQuerySerializationGroupsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resourceObjectTypeQuerySerializationGroupsProvider')] public function testGetResourceObjectTypeQuerySerializationGroups(string $itemSerializationGroup, string $collectionSerializationGroup, Operation $operation, string $shortName): void { $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ @@ -624,9 +622,7 @@ public function testGetEnumType(): void ]), $this->typeBuilder->getEnumType($operation)); } - /** - * @dataProvider typesProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('typesProvider')] public function testIsCollection(Type $type, bool $expectedIsCollection): void { $this->assertSame($expectedIsCollection, $this->typeBuilder->isCollection($type)); diff --git a/src/GraphQl/Tests/Type/TypeConverterTest.php b/src/GraphQl/Tests/Type/TypeConverterTest.php index 15ab234b8a9..3972a9aed09 100644 --- a/src/GraphQl/Tests/Type/TypeConverterTest.php +++ b/src/GraphQl/Tests/Type/TypeConverterTest.php @@ -61,9 +61,7 @@ protected function setUp(): void $this->typeConverter = new TypeConverter($this->typeBuilderProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal()); } - /** - * @dataProvider convertTypeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('convertTypeProvider')] public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void { $this->typeBuilderProphecy->isCollection($type)->willReturn(false); @@ -169,9 +167,7 @@ public function testConvertTypeInputResource(): void $this->assertSame($expectedGraphqlType, $graphqlType); } - /** - * @dataProvider convertTypeResourceProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('convertTypeResourceProvider')] public function testConvertTypeCollectionResource(Type $type, ObjectType $expectedGraphqlType): void { $collectionOperation = new QueryCollection(); @@ -215,9 +211,7 @@ public function testConvertTypeCollectionEnum(): void $this->assertSame($expectedGraphqlType, $graphqlType); } - /** - * @dataProvider resolveTypeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resolveTypeProvider')] public function testResolveType(string $type, string|GraphQLType $expectedGraphqlType): void { $this->typesContainerProphecy->has(\DateTime::class)->willReturn(true); @@ -242,9 +236,7 @@ public static function resolveTypeProvider(): array ]; } - /** - * @dataProvider resolveTypeInvalidProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resolveTypeInvalidProvider')] public function testResolveTypeInvalid(string $type, string $expectedExceptionMessage): void { $this->typesContainerProphecy->has('UnknownType')->willReturn(false); diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 438602ee70d..09529299815 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -16,7 +16,6 @@ use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\GraphQl\Exception\InvalidTypeException; -use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; use ApiPlatform\Metadata\FilterInterface; @@ -25,6 +24,7 @@ use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\InflectorInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -48,18 +48,12 @@ * * @author Alan Poulain */ -final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface +final class FieldsBuilder implements FieldsBuilderEnumInterface { - private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder; + private readonly ContextAwareTypeBuilderInterface $typeBuilder; - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ?ResolverFactoryInterface $collectionResolverFactory, private readonly ?ResolverFactoryInterface $itemMutationResolverFactory, private readonly ?ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector()) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $resolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector()) { - if ($typeBuilder instanceof TypeBuilderInterface) { - @trigger_error(\sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); - } - if ($typeBuilder instanceof TypeBuilderEnumInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED); - } $this->typeBuilder = $typeBuilder; } @@ -73,7 +67,7 @@ public function getNodeQueryFields(): array 'args' => [ 'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())], ], - 'resolve' => ($this->itemResolverFactory)(), + 'resolve' => ($this->resolverFactory)(), ]; } @@ -425,22 +419,10 @@ private function getResourceFieldConfiguration(?string $property, ?string $field } } - if ($this->itemResolverFactory instanceof ResolverFactory) { - if ($isStandardGraphqlType || $input) { - $resolve = null; - } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory); - } + if ($isStandardGraphqlType || $input) { + $resolve = null; } else { - if ($isStandardGraphqlType || $input) { - $resolve = null; - } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } + $resolve = ($this->resolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory); } return [ @@ -498,6 +480,21 @@ private function getParameterArgs(Operation $operation, array $args = []): array $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); } + + if ($filter instanceof OpenApiParameterFilterInterface) { + foreach ($filter->getOpenApiParameters($parameter) as $value) { + $values = []; + parse_str($value->getName(), $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName()); + } } return $args; @@ -678,11 +675,6 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati if ($this->typeBuilder->isCollection($type)) { if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { - // Deprecated path, to remove in API Platform 4. - if ($this->typeBuilder instanceof TypeBuilderInterface) { - return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation); - } - return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation); } diff --git a/src/GraphQl/Type/FieldsBuilderInterface.php b/src/GraphQl/Type/FieldsBuilderInterface.php deleted file mode 100644 index dc4bd57f003..00000000000 --- a/src/GraphQl/Type/FieldsBuilderInterface.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Interface implemented to build GraphQL fields. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.1. Use @see FieldsBuilderEnumInterface instead. - */ -interface FieldsBuilderInterface -{ - /** - * Gets the fields of a node for a query. - */ - public function getNodeQueryFields(): array; - - /** - * Gets the item query fields of the schema. - */ - public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array; - - /** - * Gets the collection query fields of the schema. - */ - public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array; - - /** - * Gets the mutation fields of the schema. - */ - public function getMutationFields(string $resourceClass, Operation $operation): array; - - /** - * Gets the subscription fields of the schema. - */ - public function getSubscriptionFields(string $resourceClass, Operation $operation): array; - - /** - * Gets the fields of the type of the given resource. - */ - public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array; - - /** - * Resolve the args of a resource by resolving its types. - */ - public function resolveResourceArgs(array $args, Operation $operation): array; -} diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index dd91a2e3c9e..544ec35acbf 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -32,11 +32,8 @@ */ final class SchemaBuilder implements SchemaBuilderInterface { - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface $fieldsBuilder) { - if ($this->fieldsBuilder instanceof FieldsBuilderInterface) { - @trigger_error(\sprintf('$fieldsBuilder argument of SchemaBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); - } } public function getSchema(): Schema diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index b900e97622f..0ffac895eeb 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -41,11 +41,17 @@ final class TypeBuilder implements ContextAwareTypeBuilderInterface { private $defaultFieldResolver; - public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private readonly ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination) + public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private ?ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination) { + $this->fieldsBuilderLocator = $fieldsBuilderLocator; $this->defaultFieldResolver = $defaultFieldResolver; } + public function setFieldsBuilderLocator(ContainerInterface $fieldsBuilderLocator): void + { + $this->fieldsBuilderLocator = $fieldsBuilderLocator; + } + /** * {@inheritdoc} */ @@ -195,15 +201,10 @@ public function getEnumType(Operation $operation): GraphQLType return $this->typesContainer->get($enumName); } - /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */ + /** @var FieldsBuilderEnumInterface $fieldsBuilder */ $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); $enumCases = []; - // Remove the condition in API Platform 4. - if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) { - $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); - } else { - @trigger_error(\sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); - } + $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); $enumConfig = [ 'name' => $enumName, diff --git a/src/GraphQl/Type/TypeBuilderEnumInterface.php b/src/GraphQl/Type/TypeBuilderEnumInterface.php deleted file mode 100644 index 6e674f452d7..00000000000 --- a/src/GraphQl/Type/TypeBuilderEnumInterface.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; - -/** - * Interface implemented to build a GraphQL type. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.3. Use @see ContextAwareTypeBuilderInterface instead. - */ -interface TypeBuilderEnumInterface -{ - /** - * Gets the object type of the given resource. - * - * @return GraphQLType the object type, possibly wrapped by NonNull - */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0, ?ApiProperty $propertyMetadata = null): GraphQLType; - - /** - * Get the interface type of a node. - */ - public function getNodeInterface(): InterfaceType; - - /** - * Gets the type of a paginated collection of the given resource type. - */ - public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; - - /** - * Gets the type corresponding to an enum. - */ - public function getEnumType(Operation $operation): GraphQLType; - - /** - * Returns true if a type is a collection. - */ - public function isCollection(Type $type): bool; -} diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php deleted file mode 100644 index 8b782e32461..00000000000 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; - -/** - * Interface implemented to build a GraphQL type. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface instead. - */ -interface TypeBuilderInterface -{ - /** - * Gets the object type of the given resource. - * - * @return ObjectType|NonNull the object type, possibly wrapped by NonNull - */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; - - /** - * Get the interface type of a node. - */ - public function getNodeInterface(): InterfaceType; - - /** - * Gets the type of a paginated collection of the given resource type. - * - * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface::getPaginatedCollectionType() method instead. - */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; - - /** - * Returns true if a type is a collection. - */ - public function isCollection(Type $type): bool; -} diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 69baa00b209..d76d6eb2617 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -37,15 +37,8 @@ */ final class TypeConverter implements TypeConverterInterface { - public function __construct(private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) + public function __construct(private readonly ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) { - if ($typeBuilder instanceof TypeBuilderInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); - } - - if ($typeBuilder instanceof TypeBuilderEnumInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED); - } } /** @@ -133,22 +126,19 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati if (!$hasGraphQl) { if (is_a($resourceClass, \BackedEnum::class, true)) { - // Remove the condition in API Platform 4. - if ($this->typeBuilder instanceof TypeBuilderEnumInterface || $this->typeBuilder instanceof ContextAwareTypeBuilderInterface) { - $operation = null; - try { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $operation = $resourceMetadataCollection->getOperation(); - } catch (ResourceClassNotFoundException|OperationNotFoundException) { - } - /** @var Query $enumOperation */ - $enumOperation = (new Query()) - ->withClass($resourceClass) - ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName()) - ->withDescription($operation?->getDescription()); - - return $this->typeBuilder->getEnumType($enumOperation); + $operation = null; + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $operation = $resourceMetadataCollection->getOperation(); + } catch (ResourceClassNotFoundException|OperationNotFoundException) { } + /** @var Query $enumOperation */ + $enumOperation = (new Query()) + ->withClass($resourceClass) + ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName()) + ->withDescription($operation?->getDescription()); + + return $this->typeBuilder->getEnumType($enumOperation); } return null; @@ -178,19 +168,21 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati try { $operation = $resourceMetadataCollection->getOperation($operationName); } catch (OperationNotFoundException) { - $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query'); + try { + $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query'); + } catch (OperationNotFoundException) { + throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $isCollection ? 'collection_query' : 'item_query', $resourceClass)); + } } if (!$operation instanceof Operation) { - throw new OperationNotFoundException(); + throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $operationName, $resourceClass)); } - return $this->typeBuilder instanceof ContextAwareTypeBuilderInterface ? - $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [ - 'input' => $input, - 'wrapped' => false, - 'depth' => $depth, - ]) : - $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); + return $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [ + 'input' => $input, + 'wrapped' => false, + 'depth' => $depth, + ]); } private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType diff --git a/src/GraphQl/composer.json b/src/GraphQl/composer.json index a4373fb154f..f676ddae850 100644 --- a/src/GraphQl/composer.json +++ b/src/GraphQl/composer.json @@ -20,25 +20,22 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", "api-platform/serializer": "^3.4 || ^4.0", - "api-platform/validator": "^3.4 || ^4.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", - "webonyx/graphql-php": "^14.0 || ^15.0", - "willdurand/negotiation": "^3.0" + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "webonyx/graphql-php": "^15.0", + "willdurand/negotiation": "^3.1" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", + "phpspec/prophecy-phpunit": "^2.2", "api-platform/validator": "^3.4 || ^4.0", - "twig/twig": "^3.7", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "symfony/mercure-bundle": "*", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/routing": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -50,7 +47,8 @@ }, "suggest": { "api-platform/doctrine-odm": "To support doctrine ODM state options.", - "api-platform/doctrine-orm": "To support doctrine ORM state options." + "api-platform/doctrine-orm": "To support doctrine ORM state options.", + "api-platform/validator": "To support validation." }, "config": { "preferred-install": { @@ -69,7 +67,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/GraphQl/phpunit.xml.dist b/src/GraphQl/phpunit.xml.dist index 0e1e002f892..ef0e1f48ae8 100644 --- a/src/GraphQl/phpunit.xml.dist +++ b/src/GraphQl/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + diff --git a/src/Hal/Serializer/CollectionNormalizer.php b/src/Hal/Serializer/CollectionNormalizer.php index f0fdf618f3a..7e796a6f3a7 100644 --- a/src/Hal/Serializer/CollectionNormalizer.php +++ b/src/Hal/Serializer/CollectionNormalizer.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\IriHelper; @@ -30,7 +29,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonhal'; - public function __construct(ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); } diff --git a/src/Hal/Serializer/EntrypointNormalizer.php b/src/Hal/Serializer/EntrypointNormalizer.php index 65812fba380..2756a35d41a 100644 --- a/src/Hal/Serializer/EntrypointNormalizer.php +++ b/src/Hal/Serializer/EntrypointNormalizer.php @@ -13,30 +13,25 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. * * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonhal'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly LegacyUrlGeneratorInterface|UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } @@ -75,25 +70,11 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/Hal/Serializer/ObjectNormalizer.php b/src/Hal/Serializer/ObjectNormalizer.php index 9de3609bf6b..f52903b877f 100644 --- a/src/Hal/Serializer/ObjectNormalizer.php +++ b/src/Hal/Serializer/ObjectNormalizer.php @@ -13,24 +13,20 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON HAL metadata when appropriate, but otherwise * just passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface { public const FORMAT = 'jsonhal'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter) { } @@ -44,33 +40,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/ParameterValidator/composer.json b/src/Hal/composer.json similarity index 70% rename from src/ParameterValidator/composer.json rename to src/Hal/composer.json index e382da3fb46..2da21682e20 100644 --- a/src/ParameterValidator/composer.json +++ b/src/Hal/composer.json @@ -1,17 +1,11 @@ { - "name": "api-platform/parameter-validator", - "description": "Validates parameters depending on API-Platform filter description", + "name": "api-platform/json-hal", + "description": "API Hal support", "type": "library", "keywords": [ "REST", - "GraphQL", "API", - "JSON-LD", - "Hydra", - "JSONAPI", - "OpenAPI", - "HAL", - "Swagger" + "HAL" ], "homepage": "https://api-platform.com", "license": "MIT", @@ -30,17 +24,11 @@ "php": ">=8.1", "api-platform/state": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^3.1" - }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.1", - "sebastian/comparator": "<5.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" + "api-platform/serializer": "^3.4 || ^4.0" }, "autoload": { "psr-4": { - "ApiPlatform\\ParameterValidator\\": "" + "ApiPlatform\\Hal\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -62,7 +50,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", @@ -71,5 +59,8 @@ }, "scripts": { "test": "./vendor/bin/phpunit" + }, + "require-dev": { + "phpunit/phpunit": "^11.2" } } diff --git a/src/HttpCache/EventListener/AddHeadersListener.php b/src/HttpCache/EventListener/AddHeadersListener.php deleted file mode 100644 index 516ce45e0f4..00000000000 --- a/src/HttpCache/EventListener/AddHeadersListener.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Configures cache HTTP headers for the current response. - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddHeadersListener.php instead - */ -final class AddHeadersListener -{ - use OperationRequestInitiatorTrait; - - public function __construct(private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (!$request->isMethodCacheable()) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (\count($attributes) < 1) { - return; - } - - $response = $event->getResponse(); - - if (!$response->getContent() || !$response->isSuccessful()) { - return; - } - - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $resourceCacheHeaders = $attributes['cache_headers'] ?? $operation?->getCacheHeaders() ?? []; - - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $response->getContent())); - } - - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } - - $vary = $resourceCacheHeaders['vary'] ?? $this->vary; - if (null !== $vary) { - $response->setVary(array_diff($vary, $response->getVary()), false); - } - - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - } -} diff --git a/src/HttpCache/EventListener/AddTagsListener.php b/src/HttpCache/EventListener/AddTagsListener.php deleted file mode 100644 index 74cd37ef4ee..00000000000 --- a/src/HttpCache/EventListener/AddTagsListener.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Sets the list of resources' IRIs included in this response in the configured cache tag HTTP header and/or "xkey" HTTP headers. - * - * By default the "Cache-Tags" HTTP header is used because it is supported by CloudFlare. - * - * @see https://developers.cloudflare.com/cache/how-to/purge-cache#add-cache-tag-http-response-headers - * - * The "xkey" is used because it is supported by Varnish. - * @see https://docs.varnish-software.com/varnish-cache-plus/vmods/ykey/ - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddTagsListener.php instead - */ -final class AddTagsListener -{ - use OperationRequestInitiatorTrait; - use UriVariablesResolverTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?PurgerInterface $purger = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Adds the configured HTTP cache tag and "xkey" headers. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $response = $event->getResponse(); - - if ( - !$request->isMethodCacheable() - || !$response->isCacheable() - || (!$attributes = RequestAttributesExtractor::extractAttributes($request)) - ) { - return; - } - - $resources = $request->attributes->get('_resources'); - if ($operation instanceof CollectionOperationInterface) { - // Allows to purge collections - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); - $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); - - $resources[$iri] = $iri; - } - - if (!$resources) { - return; - } - - if (!$this->purger) { - $response->headers->set('Cache-Tags', implode(',', $resources)); - - return; - } - - $headers = $this->purger->getResponseHeaders($resources); - - foreach ($headers as $key => $value) { - $response->headers->set($key, $value); - } - } -} diff --git a/src/HttpCache/State/AddHeadersProcessor.php b/src/HttpCache/State/AddHeadersProcessor.php index e1c008b7618..c11c23d7b79 100644 --- a/src/HttpCache/State/AddHeadersProcessor.php +++ b/src/HttpCache/State/AddHeadersProcessor.php @@ -53,7 +53,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $resourceCacheHeaders = $operation->getCacheHeaders() ?? []; if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $content)); + $response->setEtag(hash('xxh3', (string) $content)); } if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { diff --git a/src/HttpCache/Tests/VarnishPurgerTest.php b/src/HttpCache/Tests/VarnishPurgerTest.php index 39d4f564fcd..0cfb5883458 100644 --- a/src/HttpCache/Tests/VarnishPurgerTest.php +++ b/src/HttpCache/Tests/VarnishPurgerTest.php @@ -68,9 +68,7 @@ public function testEmptyTags(): void $purger->purge([]); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend): void { /** @var HttpClientInterface $client */ diff --git a/src/HttpCache/Tests/VarnishXKeyPurgerTest.php b/src/HttpCache/Tests/VarnishXKeyPurgerTest.php index df1653007ce..b73dc0e7458 100644 --- a/src/HttpCache/Tests/VarnishXKeyPurgerTest.php +++ b/src/HttpCache/Tests/VarnishXKeyPurgerTest.php @@ -98,9 +98,7 @@ public function testCustomGlue(): void $purger->purge(['/foo', '/bar', '/baz']); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $keysToSend): void { /** @var HttpClientInterface $client */ diff --git a/src/HttpCache/composer.json b/src/HttpCache/composer.json index e0369cbfb63..6b42bcabde8 100644 --- a/src/HttpCache/composer.json +++ b/src/HttpCache/composer.json @@ -22,18 +22,17 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", - "symfony/http-foundation": "^6.4 || ^7.1" + "symfony/http-foundation": "^6.4 || ^7.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.0 || ^7.0", "symfony/dependency-injection": "^6.4 || ^7.0", - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", + "phpspec/prophecy-phpunit": "^2.2", "symfony/http-client": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -59,7 +58,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/HttpCache/phpunit.xml.dist b/src/HttpCache/phpunit.xml.dist index d2a2213bbf7..f4da1809545 100644 --- a/src/HttpCache/phpunit.xml.dist +++ b/src/HttpCache/phpunit.xml.dist @@ -1,31 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Hydra/EventListener/AddLinkHeaderListener.php b/src/Hydra/EventListener/AddLinkHeaderListener.php deleted file mode 100644 index 397ab5ea2c4..00000000000 --- a/src/Hydra/EventListener/AddLinkHeaderListener.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\EventListener; - -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\JsonLd\ContextBuilder; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\Util\CorsTrait; -use Psr\Link\EvolvableLinkProviderInterface; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\WebLink\GenericLinkProvider; -use Symfony\Component\WebLink\Link; - -/** - * Adds the HTTP Link header pointing to the Hydra documentation. - * - * @deprecated use ApiPlatform\Hydra\State\HydraLinkProcessor instead - * - * @author Kévin Dunglas - */ -final class AddLinkHeaderListener -{ - use CorsTrait; - - public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) - { - } - - /** - * Sends the Hydra header on each response. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (($operation = $request->attributes->get('_api_operation')) && 'api_platform.symfony.main_controller' === $operation->getController()) { - return; - } - - // Prevent issues with NelmioCorsBundle - if ($this->isPreflightRequest($request)) { - return; - } - - $apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL); - $apiDocLink = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl); - $linkProvider = $request->attributes->get('_api_platform_links', new GenericLinkProvider()); - - if (!$linkProvider instanceof EvolvableLinkProviderInterface) { - return; - } - - foreach ($linkProvider->getLinks() as $link) { - if ($link->getHref() === $apiDocUrl) { - return; - } - } - - $request->attributes->set('_api_platform_links', $linkProvider->withLink($apiDocLink)); - } -} diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index d44cfb29b1d..b6595a4fde1 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -13,31 +13,26 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\FilterInterface; -use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\Parameters; use ApiPlatform\Metadata\QueryParameterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Psr\Container\ContainerInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Enhances the result of collection by adding the filters applied on collection. * * @author Samuel ROZE */ -final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface +final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface { use HydraPrefixTrait; private ?ContainerInterface $filterLocator = null; @@ -46,8 +41,13 @@ final class CollectionFiltersNormalizer implements NormalizerInterface, Normaliz * @param ContainerInterface $filterLocator The new filter locator or the deprecated filter collection * @param array $defaultContext */ - public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, ContainerInterface $filterLocator, private readonly array $defaultContext = []) - { + public function __construct( + private readonly NormalizerInterface $collectionNormalizer, + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ?ContainerInterface $filterLocator = null, + private readonly array $defaultContext = [], + ) { $this->filterLocator = $filterLocator; } @@ -61,28 +61,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) { - return ['*' => $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod()]; - } - return $this->collectionNormalizer->getSupportedTypes($format); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ @@ -151,10 +132,9 @@ public function setNormalizer(NormalizerInterface $normalizer): void /** * Returns the content of the Hydra search property. * - * @param FilterInterface[] $filters - * @param array $parameters + * @param FilterInterface[] $filters */ - private function getSearch(string $resourceClass, array $parts, array $filters, array|Parameters|null $parameters, string $hydraPrefix): array + private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array { $variables = []; $mapping = []; @@ -171,13 +151,19 @@ private function getSearch(string $resourceClass, array $parts, array $filters, continue; } - if (($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) { - foreach ($filter->getDescription($resourceClass) as $variable => $description) { - // This is a practice induced by PHP and is not necessary when implementing URI template + if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) { + $filterDescription = $filter->getDescription($resourceClass); + + foreach ($filterDescription as $variable => $description) { + // // This is a practice induced by PHP and is not necessary when implementing URI template if (str_ends_with((string) $variable, '[]')) { continue; } + if (($prop = $parameter->getProperty()) && ($description['property'] ?? null) !== $prop) { + continue; + } + // :property is a pattern allowed when defining parameters $k = str_replace(':property', $description['property'], $key); $variable = str_replace($description['property'], $k, $variable); @@ -189,7 +175,9 @@ private function getSearch(string $resourceClass, array $parts, array $filters, $mapping[] = $m; } - continue; + if ($filterDescription) { + continue; + } } if (!($property = $parameter->getProperty())) { diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php index ca8992af18c..8485789f0a2 100644 --- a/src/Hydra/Serializer/CollectionNormalizer.php +++ b/src/Hydra/Serializer/CollectionNormalizer.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\JsonLd\Serializer\JsonLdContextTrait; @@ -43,7 +41,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer self::IRI_ONLY => false, ]; - public function __construct(private readonly ContextBuilderInterface $contextBuilder, LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, private readonly LegacyIriConverterInterface|IriConverterInterface $iriConverter, readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = []) + public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = []) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); diff --git a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php index c4e43b667d6..0b24fcb0fa0 100644 --- a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php @@ -13,9 +13,7 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; -use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -29,7 +27,7 @@ final class ConstraintViolationListNormalizer extends AbstractConstraintViolatio use HydraPrefixTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, ?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null, private readonly ?array $defaultContext = []) + public function __construct(?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($serializePayloadFields, $nameConverter); } @@ -39,21 +37,8 @@ public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGene */ public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - [$messages, $violations] = $this->getMessagesAndViolations($object); - - // TODO: in api platform 4 this will be the default, as right now we serialize a ValidationException instead of a ConstraintViolationList - if ($context['rfc_7807_compliant_errors'] ?? false) { - return $violations; - } - - $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); - - return [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']), - '@type' => 'ConstraintViolationList', - $hydraPrefix.'title' => $context['title'] ?? 'An error occurred', - $hydraPrefix.'description' => $messages ? implode("\n", $messages) : (string) $object, - 'violations' => $violations, - ]; + [, $violations] = $this->getMessagesAndViolations($object); + + return $violations; } } diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index ba790ab4284..510e5f48c56 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; use ApiPlatform\Documentation\Documentation; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonLd\ContextBuilderInterface; @@ -31,26 +29,33 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; + +use const ApiPlatform\JsonLd\HYDRA_CONTEXT; /** * Creates a machine readable Hydra API documentation. * * @author Kévin Dunglas */ -final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class DocumentationNormalizer implements NormalizerInterface { use HydraPrefixTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly ?NameConverterInterface $nameConverter = null, private readonly ?array $defaultContext = []) - { + public function __construct( + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + private readonly ResourceClassResolverInterface $resourceClassResolver, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly ?NameConverterInterface $nameConverter = null, + private readonly ?array $defaultContext = [], + ) { } /** @@ -71,6 +76,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } $shortName = $resourceMetadata->getShortName(); + $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName"; $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection); $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection); @@ -240,8 +246,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) { continue; } - - $hydraOperations[] = $this->getHydraOperation($operation, $operation->getTypes()[0] ?? "#{$operation->getShortName()}", $hydraPrefix); + $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix); } } @@ -401,7 +406,8 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool foreach ($builtInTypes as $type) { $className = $type->getClassName(); - if (!$type->isCollection() + if ( + !$type->isCollection() && null !== $className && $this->resourceClassResolver->isResourceClass($className) ) { @@ -426,7 +432,7 @@ private function getClasses(array $entrypointProperties, array $classes, string '@type' => $hydraPrefix.'Operation', $hydraPrefix.'method' => 'GET', 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', + 'returns' => 'EntryPoint', ], ]; @@ -569,18 +575,19 @@ private function computeDoc(Documentation $object, array $classes, string $hydra private function getContext(string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { return [ - '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', - 'hydra' => ContextBuilderInterface::HYDRA_NS, - 'rdf' => ContextBuilderInterface::RDF_NS, - 'rdfs' => ContextBuilderInterface::RDFS_NS, - 'xmls' => ContextBuilderInterface::XML_NS, - 'owl' => ContextBuilderInterface::OWL_NS, - 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, - 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], - 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], - 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], - 'expects' => ['@id' => $hydraPrefix.'expects', '@type' => '@id'], - 'returns' => ['@id' => $hydraPrefix.'returns', '@type' => '@id'], + HYDRA_CONTEXT, + [ + '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', + 'hydra' => ContextBuilderInterface::HYDRA_NS, + 'rdf' => ContextBuilderInterface::RDF_NS, + 'rdfs' => ContextBuilderInterface::RDFS_NS, + 'xmls' => ContextBuilderInterface::XML_NS, + 'owl' => ContextBuilderInterface::OWL_NS, + 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, + 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], + 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], + 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], + ], ]; } @@ -596,18 +603,4 @@ public function getSupportedTypes($format): array { return self::FORMAT === $format ? [Documentation::class => true] : []; } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } } diff --git a/src/Hydra/Serializer/EntrypointNormalizer.php b/src/Hydra/Serializer/EntrypointNormalizer.php index 2abde52ee82..19bf8574a29 100644 --- a/src/Hydra/Serializer/EntrypointNormalizer.php +++ b/src/Hydra/Serializer/EntrypointNormalizer.php @@ -13,30 +13,25 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. * * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonld'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } @@ -84,25 +79,11 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php deleted file mode 100644 index af9f9bee740..00000000000 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\Serializer; - -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\State\ApiResource\Error; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; - -/** - * Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation. - * - * @deprecated Errors are resources since API Platform 3.2 we use the ItemNormalizer - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ErrorNormalizerTrait; - use HydraPrefixTrait; - - public const FORMAT = 'jsonld'; - public const TITLE = 'title'; - private array $defaultContext = [self::TITLE => 'An error occurred']; - - public function __construct(private readonly LegacyUrlGeneratorInterface|UrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = []) - { - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - } - - /** - * {@inheritdoc} - */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null - { - $hydraPrefix = $this->getHydraPrefix($context); - $data = [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']), - '@type' => $hydraPrefix.'Error', - $hydraPrefix.'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - $hydraPrefix.'description' => $this->getErrorMessage($object, $context, $this->debug), - ]; - - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { - if ($context['api_error_resource'] ?? false) { - return false; - } - - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); - } - - public function getSupportedTypes($format): array - { - if (self::FORMAT === $format) { - return [ - \Exception::class => true, - Error::class => false, - FlattenException::class => true, - ]; - } - - return []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } -} diff --git a/src/Hydra/Serializer/ErrorNormalizerTrait.php b/src/Hydra/Serializer/ErrorNormalizerTrait.php deleted file mode 100644 index 12fec8ff0fc..00000000000 --- a/src/Hydra/Serializer/ErrorNormalizerTrait.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\Serializer; - -use ApiPlatform\Exception\ErrorCodeSerializableInterface; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Response; - -/** - * @internal - */ -trait ErrorNormalizerTrait -{ - private function getErrorMessage($object, array $context, bool $debug = false): string - { - $message = $object->getMessage(); - - if ($debug) { - return $message; - } - - if ($object instanceof FlattenException) { - $statusCode = $context['statusCode'] ?? $object->getStatusCode(); - if ($statusCode >= 500 && $statusCode < 600) { - $message = Response::$statusTexts[$statusCode] ?? Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR]; - } - } - - return $message; - } - - private function getErrorCode(object $object): ?string - { - if ($object instanceof FlattenException) { - $exceptionClass = $object->getClass(); - } else { - $exceptionClass = $object::class; - } - - if (is_a($exceptionClass, ErrorCodeSerializableInterface::class, true)) { - return $exceptionClass::getErrorCode(); - } - - return null; - } -} diff --git a/src/Hydra/Serializer/HydraPrefixNameConverter.php b/src/Hydra/Serializer/HydraPrefixNameConverter.php index d308f5846f9..370255bde89 100644 --- a/src/Hydra/Serializer/HydraPrefixNameConverter.php +++ b/src/Hydra/Serializer/HydraPrefixNameConverter.php @@ -17,14 +17,18 @@ use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -final class HydraPrefixNameConverter implements NameConverterInterface, AdvancedNameConverterInterface +final readonly class HydraPrefixNameConverter implements NameConverterInterface, AdvancedNameConverterInterface { - public function __construct(private readonly NameConverterInterface $nameConverter) + /** + * @param array $defaultContext + */ + public function __construct(private NameConverterInterface $nameConverter, private array $defaultContext = []) { } public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string { + $context += $this->defaultContext; $name = $this->nameConverter->normalize($propertyName, $class, $format, $context); if (true === ($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) { diff --git a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php index 4eeee8be2db..59932f2e2f4 100644 --- a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php +++ b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php @@ -18,16 +18,13 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\IriHelper; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Adds a view key to the result of a paginated Hydra collection. @@ -35,7 +32,7 @@ * @author Kévin Dunglas * @author Samuel ROZE */ -final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface +final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface { use HydraPrefixTrait; private readonly PropertyAccessorInterface $propertyAccessor; @@ -122,30 +119,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) { - return [ - '*' => $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(), - ]; - } - return $this->collectionNormalizer->getSupportedTypes($format); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/Hydra/Tests/Fixtures/CustomConverter.php b/src/Hydra/Tests/Fixtures/CustomConverter.php new file mode 100644 index 00000000000..405f1dab4cb --- /dev/null +++ b/src/Hydra/Tests/Fixtures/CustomConverter.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Custom converter that will only convert a property named "nameConverted" + * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. + */ +class CustomConverter implements AdvancedNameConverterInterface +{ + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; + } + + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; + } +} diff --git a/src/ParameterValidator/Tests/Fixtures/Dummy.php b/src/Hydra/Tests/Fixtures/Dummy.php similarity index 83% rename from src/ParameterValidator/Tests/Fixtures/Dummy.php rename to src/Hydra/Tests/Fixtures/Dummy.php index a87df579665..bf355e87577 100644 --- a/src/ParameterValidator/Tests/Fixtures/Dummy.php +++ b/src/Hydra/Tests/Fixtures/Dummy.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\ParameterValidator\Tests\Fixtures; +namespace ApiPlatform\Hydra\Tests\Fixtures; class Dummy { diff --git a/src/Hydra/Tests/Fixtures/Entity/SoMany.php b/src/Hydra/Tests/Fixtures/Entity/SoMany.php new file mode 100644 index 00000000000..493928369fb --- /dev/null +++ b/src/Hydra/Tests/Fixtures/Entity/SoMany.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource(paginationPartial: true, paginationViaCursor: [['field' => 'id', 'direction' => 'DESC']])] +#[ORM\Entity] +class SoMany +{ + public $id; + public $content; +} diff --git a/src/Exception/InvalidValueException.php b/src/Hydra/Tests/Fixtures/Foo.php similarity index 69% rename from src/Exception/InvalidValueException.php rename to src/Hydra/Tests/Fixtures/Foo.php index dc9d834a644..e720d223926 100644 --- a/src/Exception/InvalidValueException.php +++ b/src/Hydra/Tests/Fixtures/Foo.php @@ -11,11 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Exception; +namespace ApiPlatform\Hydra\Tests\Fixtures; -/** - * @deprecated - */ -class InvalidValueException extends InvalidArgumentException +class Foo { + public int $id; + public string $bar; } diff --git a/src/Hydra/Tests/Fixtures/NotAResource.php b/src/Hydra/Tests/Fixtures/NotAResource.php new file mode 100644 index 00000000000..4906cf53fb1 --- /dev/null +++ b/src/Hydra/Tests/Fixtures/NotAResource.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * This class is not mapped as an API resource. + * + * @author Kévin Dunglas + */ +class NotAResource +{ + public function __construct( + #[Groups('contain_non_resource')] + private $foo, + #[Groups('contain_non_resource')] + private $bar, + ) { + } + + public function getFoo() + { + return $this->foo; + } + + public function getBar() + { + return $this->bar; + } +} diff --git a/tests/Hydra/JsonSchema/SchemaFactoryTest.php b/src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php similarity index 98% rename from tests/Hydra/JsonSchema/SchemaFactoryTest.php rename to src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php index 20ffd9cd148..e8d735ab68d 100644 --- a/tests/Hydra/JsonSchema/SchemaFactoryTest.php +++ b/src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php @@ -11,9 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\JsonSchema; +namespace ApiPlatform\Hydra\Tests\JsonSchema; use ApiPlatform\Hydra\JsonSchema\SchemaFactory; +use ApiPlatform\Hydra\Tests\Fixtures\Dummy; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; @@ -26,7 +27,6 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -54,7 +54,6 @@ protected function setUp(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $baseSchemaFactory = new BaseSchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryCollection->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), propertyMetadataFactory: $propertyMetadataFactory->reveal(), diff --git a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php b/src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php similarity index 97% rename from tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php rename to src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php index 9e3de3267c5..6943b8db13c 100644 --- a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php @@ -11,21 +11,21 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; -use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer; use ApiPlatform\Hydra\Serializer\CollectionNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Dummy; +use ApiPlatform\Hydra\Tests\Fixtures\Foo; +use ApiPlatform\Hydra\Tests\Fixtures\NotAResource; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\Foo; -use ApiPlatform\Tests\Fixtures\NotAResource; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,9 +40,6 @@ class CollectionFiltersNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportsNormalization(): void { $decoratedProphecy = $this->prophesize(NormalizerInterface::class); diff --git a/tests/Hydra/Serializer/CollectionNormalizerTest.php b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php similarity index 97% rename from tests/Hydra/Serializer/CollectionNormalizerTest.php rename to src/Hydra/Tests/Serializer/CollectionNormalizerTest.php index 137d2df7835..9dfc068a5ea 100644 --- a/tests/Hydra/Serializer/CollectionNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php @@ -11,9 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; use ApiPlatform\Hydra\Serializer\CollectionNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Foo; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonLd\ContextBuilderInterface; use ApiPlatform\Metadata\IriConverterInterface; @@ -22,12 +23,10 @@ use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use ApiPlatform\Tests\Fixtures\Foo; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -38,9 +37,6 @@ class CollectionNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportsNormalize(): void { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); @@ -62,10 +58,6 @@ public function testSupportsNormalize(): void 'native-array' => true, '\Traversable' => true, ], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalizeResourceCollection(): void diff --git a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php b/src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php similarity index 63% rename from tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php rename to src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php index 77cb6c01bb9..96f1532f898 100644 --- a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php @@ -11,16 +11,14 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Hydra\Serializer\ConstraintViolationListNormalizer; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; @@ -33,15 +31,11 @@ class ConstraintViolationNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportNormalization(): void { - $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); - $normalizer = new ConstraintViolationListNormalizer($urlGeneratorProphecy->reveal(), [], $nameConverterProphecy->reveal()); + $normalizer = new ConstraintViolationListNormalizer([], $nameConverterProphecy->reveal()); $this->assertTrue($normalizer->supportsNormalization(new ConstraintViolationList(), ConstraintViolationListNormalizer::FORMAT, ['api_error_resource' => true])); $this->assertFalse($normalizer->supportsNormalization(new ConstraintViolationList(), 'xml', ['api_error_resource' => true])); @@ -49,21 +43,12 @@ public function testSupportNormalization(): void $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); $this->assertEmpty($normalizer->getSupportedTypes('json')); $this->assertSame([ConstraintViolationListInterface::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } - /** - * @dataProvider nameConverterAndPayloadFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('nameConverterAndPayloadFieldsProvider')] public function testNormalize(callable $nameConverterFactory, ?array $fields, array $expected): void { - $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); - $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList'])->willReturn('/context/foo')->shouldBeCalled(); - - $normalizer = new ConstraintViolationListNormalizer($urlGeneratorProphecy->reveal(), $fields, $nameConverterFactory($this)); + $normalizer = new ConstraintViolationListNormalizer($fields, $nameConverterFactory($this)); // Note : we use NotNull constraint and not Constraint class because Constraint is abstract $constraint = new NotNull(); @@ -79,40 +64,28 @@ public function testNormalize(callable $nameConverterFactory, ?array $fields, ar public static function nameConverterAndPayloadFieldsProvider(): iterable { $basicExpectation = [ - '@context' => '/context/foo', - '@type' => 'ConstraintViolationList', - 'hydra:title' => 'An error occurred', - 'hydra:description' => "d: a\n4: 1", - 'violations' => [ - [ - 'propertyPath' => 'd', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - ], - [ - 'propertyPath' => '4', - 'message' => '1', - 'code' => null, - ], + [ + 'propertyPath' => 'd', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '4', + 'message' => '1', + 'code' => null, ], ]; $nameConverterBasedExpectation = [ - '@context' => '/context/foo', - '@type' => 'ConstraintViolationList', - 'hydra:title' => 'An error occurred', - 'hydra:description' => "_d: a\n_4: 1", - 'violations' => [ - [ - 'propertyPath' => '_d', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - ], - [ - 'propertyPath' => '_4', - 'message' => '1', - 'code' => null, - ], + [ + 'propertyPath' => '_d', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '_4', + 'message' => '1', + 'code' => null, ], ]; @@ -133,19 +106,19 @@ public static function nameConverterAndPayloadFieldsProvider(): iterable $nullNameConverterFactory = fn () => null; $expected = $nameConverterBasedExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning']; + $expected[0]['payload'] = ['severity' => 'warning']; yield [$advancedNameConverterFactory, ['severity', 'anotherField1'], $expected]; yield [$nameConverterFactory, ['severity', 'anotherField1'], $expected]; $expected = $basicExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning']; + $expected[0]['payload'] = ['severity' => 'warning']; yield [$nullNameConverterFactory, ['severity', 'anotherField1'], $expected]; $expected = $nameConverterBasedExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; yield [$advancedNameConverterFactory, null, $expected]; yield [$nameConverterFactory, null, $expected]; $expected = $basicExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; yield [$nullNameConverterFactory, null, $expected]; yield [$advancedNameConverterFactory, [], $nameConverterBasedExpectation]; diff --git a/tests/Hydra/Serializer/DocumentationNormalizerTest.php b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php similarity index 91% rename from tests/Hydra/Serializer/DocumentationNormalizerTest.php rename to src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php index b4a159c2104..f82f796a7fe 100644 --- a/tests/Hydra/Serializer/DocumentationNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php @@ -11,12 +11,11 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Documentation\Documentation; use ApiPlatform\Hydra\Serializer\DocumentationNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\CustomConverter; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; @@ -31,12 +30,14 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\Resource\ResourceNameCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyInfo\Type; -use Symfony\Component\Serializer\Serializer; + +use const ApiPlatform\JsonLd\HYDRA_CONTEXT; /** * @author Amrouche Hamza @@ -45,9 +46,6 @@ class DocumentationNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testNormalize(): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -110,32 +108,27 @@ private function doTestNormalize($resourceMetadataFactory = null): void $expected = [ '@context' => [ - '@vocab' => '/doc#', - 'hydra' => 'http://www.w3.org/ns/hydra/core#', - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', - 'xmls' => 'http://www.w3.org/2001/XMLSchema#', - 'owl' => 'http://www.w3.org/2002/07/owl#', - 'schema' => 'https://schema.org/', - 'domain' => [ - '@id' => 'rdfs:domain', - '@type' => '@id', - ], - 'range' => [ - '@id' => 'rdfs:range', - '@type' => '@id', - ], - 'subClassOf' => [ - '@id' => 'rdfs:subClassOf', - '@type' => '@id', - ], - 'expects' => [ - '@id' => 'hydra:expects', - '@type' => '@id', - ], - 'returns' => [ - '@id' => 'hydra:returns', - '@type' => '@id', + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], ], ], '@id' => '/doc', @@ -230,23 +223,23 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:method' => 'GET', 'hydra:title' => 'foobar', 'rdfs:label' => 'foobar', - 'returns' => '#dummy', + 'returns' => 'dummy', 'hydra:foo' => 'bar', ], [ '@type' => ['hydra:Operation', 'schema:ReplaceAction'], - 'expects' => '#dummy', + 'expects' => 'dummy', 'hydra:method' => 'PUT', 'hydra:title' => 'Replaces the dummy resource.', 'rdfs:label' => 'Replaces the dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], [ '@type' => ['hydra:Operation', 'schema:FindAction'], 'hydra:method' => 'GET', 'hydra:title' => 'Retrieves a relatedDummy resource.', 'rdfs:label' => 'Retrieves a relatedDummy resource.', - 'returns' => '#relatedDummy', + 'returns' => 'relatedDummy', ], ], ], @@ -281,11 +274,11 @@ private function doTestNormalize($resourceMetadataFactory = null): void ], [ '@type' => ['hydra:Operation', 'schema:CreateAction'], - 'expects' => '#dummy', + 'expects' => 'dummy', 'hydra:method' => 'POST', 'hydra:title' => 'Creates a dummy resource.', 'rdfs:label' => 'Creates a dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], ], ], @@ -298,7 +291,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void '@type' => 'hydra:Operation', 'hydra:method' => 'GET', 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', + 'returns' => 'EntryPoint', ], ], [ @@ -367,10 +360,6 @@ private function doTestNormalize($resourceMetadataFactory = null): void $this->assertFalse($documentationNormalizer->supportsNormalization($documentation, 'hal')); $this->assertEmpty($documentationNormalizer->getSupportedTypes('json')); $this->assertSame([Documentation::class => true], $documentationNormalizer->getSupportedTypes($documentationNormalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($documentationNormalizer->hasCacheableSupportsMethod()); - } } public function testNormalizeInputOutputClass(): void @@ -419,32 +408,27 @@ public function testNormalizeInputOutputClass(): void $expected = [ '@context' => [ - '@vocab' => '/doc#', - 'hydra' => 'http://www.w3.org/ns/hydra/core#', - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', - 'xmls' => 'http://www.w3.org/2001/XMLSchema#', - 'owl' => 'http://www.w3.org/2002/07/owl#', - 'schema' => 'https://schema.org/', - 'domain' => [ - '@id' => 'rdfs:domain', - '@type' => '@id', - ], - 'range' => [ - '@id' => 'rdfs:range', - '@type' => '@id', - ], - 'subClassOf' => [ - '@id' => 'rdfs:subClassOf', - '@type' => '@id', - ], - 'expects' => [ - '@id' => 'hydra:expects', - '@type' => '@id', - ], - 'returns' => [ - '@id' => 'hydra:returns', - '@type' => '@id', + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], ], ], '@id' => '/doc', @@ -529,7 +513,7 @@ public function testNormalizeInputOutputClass(): void 'hydra:method' => 'GET', 'hydra:title' => 'Retrieves a dummy resource.', 'rdfs:label' => 'Retrieves a dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], [ '@type' => [ @@ -540,7 +524,7 @@ public function testNormalizeInputOutputClass(): void 'hydra:method' => 'PUT', 'hydra:title' => 'Replaces the dummy resource.', 'rdfs:label' => 'Replaces the dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], ], 'hydra:description' => 'dummy', @@ -588,7 +572,7 @@ public function testNormalizeInputOutputClass(): void 'hydra:Operation', 'schema:CreateAction', ], - 'expects' => '#dummy', + 'expects' => 'dummy', 'hydra:method' => 'POST', 'hydra:title' => 'Creates a dummy resource.', 'rdfs:label' => 'Creates a dummy resource.', @@ -605,7 +589,7 @@ public function testNormalizeInputOutputClass(): void '@type' => 'hydra:Operation', 'hydra:method' => 'GET', 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', + 'returns' => 'EntryPoint', ], ], 2 => [ @@ -784,32 +768,27 @@ public function testNormalizeWithoutPrefix(): void $expected = [ '@context' => [ - '@vocab' => '/doc#', - 'hydra' => 'http://www.w3.org/ns/hydra/core#', - 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', - 'xmls' => 'http://www.w3.org/2001/XMLSchema#', - 'owl' => 'http://www.w3.org/2002/07/owl#', - 'schema' => 'https://schema.org/', - 'domain' => [ - '@id' => 'rdfs:domain', - '@type' => '@id', - ], - 'range' => [ - '@id' => 'rdfs:range', - '@type' => '@id', - ], - 'subClassOf' => [ - '@id' => 'rdfs:subClassOf', - '@type' => '@id', - ], - 'expects' => [ - '@id' => 'expects', - '@type' => '@id', - ], - 'returns' => [ - '@id' => 'returns', - '@type' => '@id', + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], ], ], '@id' => '/doc', @@ -904,23 +883,23 @@ public function testNormalizeWithoutPrefix(): void 'method' => 'GET', 'title' => 'foobar', 'rdfs:label' => 'foobar', - 'returns' => '#dummy', + 'returns' => 'dummy', 'foo' => 'bar', ], [ '@type' => ['Operation', 'schema:ReplaceAction'], - 'expects' => '#dummy', + 'expects' => 'dummy', 'method' => 'PUT', 'title' => 'Replaces the dummy resource.', 'rdfs:label' => 'Replaces the dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], [ '@type' => ['Operation', 'schema:FindAction'], 'method' => 'GET', 'title' => 'Retrieves a relatedDummy resource.', 'rdfs:label' => 'Retrieves a relatedDummy resource.', - 'returns' => '#relatedDummy', + 'returns' => 'relatedDummy', ], ], ], @@ -955,11 +934,11 @@ public function testNormalizeWithoutPrefix(): void ], [ '@type' => ['Operation', 'schema:CreateAction'], - 'expects' => '#dummy', + 'expects' => 'dummy', 'method' => 'POST', 'title' => 'Creates a dummy resource.', 'rdfs:label' => 'Creates a dummy resource.', - 'returns' => '#dummy', + 'returns' => 'dummy', ], ], ], @@ -972,7 +951,7 @@ public function testNormalizeWithoutPrefix(): void '@type' => 'Operation', 'method' => 'GET', 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', + 'returns' => 'EntryPoint', ], ], [ diff --git a/tests/Hydra/Serializer/EntrypointNormalizerTest.php b/src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php similarity index 92% rename from tests/Hydra/Serializer/EntrypointNormalizerTest.php rename to src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php index 69b0f7501c9..ab115fa026b 100644 --- a/tests/Hydra/Serializer/EntrypointNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php @@ -11,10 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Hydra\Serializer\EntrypointNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; @@ -29,7 +28,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Serializer; /** * @author Kévin Dunglas @@ -38,9 +36,6 @@ class EntrypointNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportNormalization(): void { $collection = new ResourceNameCollection(); @@ -56,11 +51,7 @@ public function testSupportNormalization(): void $this->assertFalse($normalizer->supportsNormalization($entrypoint, 'json')); $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), EntrypointNormalizer::FORMAT)); $this->assertEmpty($normalizer->getSupportedTypes('json')); - $this->assertSame([Entrypoint::class => true, DocumentationEntrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } + $this->assertSame([Entrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); } public function testNormalizeWithResourceMetadata(): void diff --git a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php b/src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php similarity index 98% rename from tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php rename to src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php index 8f60b7868ad..3f45a4287f2 100644 --- a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php @@ -11,9 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Entity\SoMany; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; @@ -21,7 +22,6 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -158,9 +158,6 @@ private function normalizePaginator(bool $partial = false, bool $cursor = false) return $normalizer->normalize($paginatorProphecy->reveal(), null, ['resource_class' => SoMany::class, 'operation_name' => 'get']); } - /** - * @group legacy - */ public function testSupportsNormalization(): void { $decoratedNormalizerProphecy = $this->prophesize(NormalizerInterface::class); diff --git a/tests/Hydra/State/HydraLinkProcessorTest.php b/src/Hydra/Tests/State/HydraLinkProcessorTest.php similarity index 97% rename from tests/Hydra/State/HydraLinkProcessorTest.php rename to src/Hydra/Tests/State/HydraLinkProcessorTest.php index 299d1560e0e..b5ce10a1009 100644 --- a/tests/Hydra/State/HydraLinkProcessorTest.php +++ b/src/Hydra/Tests/State/HydraLinkProcessorTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\State; +namespace ApiPlatform\Hydra\Tests\State; use ApiPlatform\Hydra\State\HydraLinkProcessor; use ApiPlatform\JsonLd\ContextBuilder; diff --git a/src/Hydra/composer.json b/src/Hydra/composer.json index 1c04e79818e..eece6d0e331 100644 --- a/src/Hydra/composer.json +++ b/src/Hydra/composer.json @@ -24,18 +24,22 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/state": "^3.4 || ^4.0", "api-platform/documentation": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/jsonld": "^3.4 || ^4.0", "api-platform/json-schema": "^3.4 || ^4.0", - "api-platform/serializer": "^3.4 || ^4.0" + "api-platform/serializer": "^3.4 || ^4.0", + "symfony/web-link": "^6.4 || ^7.0" }, "require-dev": { "api-platform/doctrine-odm": "^3.4 || ^4.0", "api-platform/doctrine-orm": "^3.4 || ^4.0", - "api-platform/doctrine-common": "^3.4 || ^4.0" + "api-platform/doctrine-common": "^3.4 || ^4.0", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -61,7 +65,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Hydra/phpunit.xml.dist b/src/Hydra/phpunit.xml.dist new file mode 100644 index 00000000000..f88ef8c906e --- /dev/null +++ b/src/Hydra/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index e40ff6a2868..6cd4d7e1e86 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\JsonApi\JsonSchema; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; @@ -106,7 +105,7 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI ], ]; - public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null) + public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null) { if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) { $this->schemaFactory->setSchemaFactory($this); diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php index f83264fec7f..e465b6ff5fe 100644 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ b/src/JsonApi/Serializer/CollectionNormalizer.php @@ -13,7 +13,6 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\IriHelper; @@ -31,7 +30,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonapi'; - public function __construct(ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); } diff --git a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php index 92afba9744f..28604a97409 100644 --- a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php +++ b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php @@ -14,10 +14,8 @@ namespace ApiPlatform\JsonApi\Serializer; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -26,7 +24,7 @@ * * @author Héctor Hurtarte */ -final class ConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ConstraintViolationListNormalizer implements NormalizerInterface { public const FORMAT = 'jsonapi'; @@ -65,20 +63,6 @@ public function getSupportedTypes($format): array return self::FORMAT === $format ? [ConstraintViolationListInterface::class => true] : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - private function getSourcePointerFromViolation(ConstraintViolationInterface $violation): string { $fieldName = $violation->getPropertyPath(); diff --git a/src/JsonApi/Serializer/EntrypointNormalizer.php b/src/JsonApi/Serializer/EntrypointNormalizer.php index 5d01464c9b7..1dd6b67e1a3 100644 --- a/src/JsonApi/Serializer/EntrypointNormalizer.php +++ b/src/JsonApi/Serializer/EntrypointNormalizer.php @@ -13,19 +13,14 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. @@ -33,11 +28,11 @@ * @author Amrouche Hamza * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonapi'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } @@ -75,25 +70,11 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 19764afa3bf..af619b81e46 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -13,32 +13,20 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Problem\Serializer\ErrorNormalizerTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation. * * @author Héctor Hurtarte */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ErrorNormalizer implements NormalizerInterface { - use ErrorNormalizerTrait; - public const FORMAT = 'jsonapi'; - public const TITLE = 'title'; - private array $defaultContext = [ - self::TITLE => 'An error occurred', - ]; - public function __construct(private readonly bool $debug = false, array $defaultContext = [], private ?NormalizerInterface $itemNormalizer = null, private ?NormalizerInterface $constraintViolationListNormalizer = null) + public function __construct(private ?NormalizerInterface $itemNormalizer = null) { - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -46,35 +34,37 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, ?string $format = null, array $context = []): array { - // TODO: in api platform 4 this will be the default, note that JSON:API is close to Problem so we should use the same normalizer - if ($context['rfc_7807_compliant_errors'] ?? false) { - if ($object instanceof LegacyConstraintViolationListAwareExceptionInterface || $object instanceof ConstraintViolationListAwareExceptionInterface) { - // TODO: return ['errors' => $this->constraintViolationListNormalizer(...)] - return $this->constraintViolationListNormalizer->normalize($object->getConstraintViolationList(), $format, $context); - } - - $jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context); - $error = $jsonApiObject['data']['attributes']; - $error['id'] = $jsonApiObject['data']['id']; - $error['type'] = $jsonApiObject['data']['id']; - - return ['errors' => [$error]]; + $jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context); + $error = $jsonApiObject['data']['attributes']; + $error['id'] = $jsonApiObject['data']['id']; + if (isset($error['type'])) { + $error['links'] = ['type' => $error['type']]; } - $data = [ - 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'description' => $this->getErrorMessage($object, $context, $this->debug), - ]; + if (!isset($error['code']) && method_exists($object, 'getId')) { + $error['code'] = $object->getId(); + } - if (null !== $errorCode = $this->getErrorCode($object)) { - $data['code'] = $errorCode; + if (!isset($error['violations'])) { + return ['errors' => [$error]]; } - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; + $errors = []; + foreach ($error['violations'] as $violation) { + $e = ['detail' => $violation['message']] + $error; + if (isset($error['links']['type'])) { + $type = $error['links']['type']; + $e['links']['type'] = \sprintf('%s/%s', $type, $violation['propertyPath']); + $e['id'] = str_replace($type, $e['links']['type'], $e['id']); + } + if (isset($e['code'])) { + $e['code'] = \sprintf('%s/%s', $error['code'], $violation['propertyPath']); + } + unset($e['violations']); + $errors[] = $e; } - return $data; + return ['errors' => $errors]; } /** @@ -96,18 +86,4 @@ public function getSupportedTypes($format): array return []; } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } } diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 770b17ee5ea..d1e5ba7de16 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -13,14 +13,13 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; @@ -28,7 +27,6 @@ use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ContextTrait; use ApiPlatform\Serializer\TagCollectorInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Exception\LogicException; @@ -56,7 +54,7 @@ final class ItemNormalizer extends AbstractItemNormalizer private array $componentsCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); } diff --git a/src/JsonApi/Serializer/ObjectNormalizer.php b/src/JsonApi/Serializer/ObjectNormalizer.php index 2dc6a4c9274..196c12aaa6b 100644 --- a/src/JsonApi/Serializer/ObjectNormalizer.php +++ b/src/JsonApi/Serializer/ObjectNormalizer.php @@ -13,28 +13,23 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON API metadata when appropriate, but otherwise * just passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use ClassInfoTrait; public const FORMAT = 'jsonapi'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { } @@ -48,30 +43,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php index e25ad566dc9..1c9e5239e8b 100644 --- a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php +++ b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\JsonApi\Serializer; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -44,6 +45,10 @@ public function normalize(string $propertyName, ?string $class = null, ?string $ $propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context); } + if ($class && is_a($class, ProblemExceptionInterface::class, true)) { + return $propertyName; + } + if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) { $propertyName = self::JSON_API_RESERVED_ATTRIBUTES[$propertyName]; } diff --git a/tests/Mock/Exception/ErrorCodeSerializable.php b/src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php similarity index 81% rename from tests/Mock/Exception/ErrorCodeSerializable.php rename to src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php index 2531b33f207..eff84141639 100644 --- a/tests/Mock/Exception/ErrorCodeSerializable.php +++ b/src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Mock\Exception; +namespace ApiPlatform\JsonApi\Tests\Fixtures; -use ApiPlatform\Exception\ErrorCodeSerializableInterface; +use ApiPlatform\Metadata\Exception\ErrorCodeSerializableInterface; class ErrorCodeSerializable extends \Exception implements ErrorCodeSerializableInterface { diff --git a/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php index 0c2548a7e07..7a642739297 100644 --- a/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php +++ b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\JsonApi\JsonSchema; +namespace ApiPlatform\JsonApi\Tests\JsonSchema; use ApiPlatform\JsonApi\JsonSchema\SchemaFactory; use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; @@ -53,7 +53,6 @@ protected function setUp(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true]); $baseSchemaFactory = new BaseSchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactory->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), propertyMetadataFactory: $propertyMetadataFactory->reveal(), diff --git a/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php b/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php index f165af89c9a..62ca83a51bc 100644 --- a/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php +++ b/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php @@ -13,20 +13,19 @@ namespace ApiPlatform\JsonApi\Tests\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\JsonApi\Serializer\CollectionNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * @author Amrouche Hamza @@ -35,9 +34,6 @@ class CollectionNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportsNormalize(): void { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); @@ -56,10 +52,6 @@ public function testSupportsNormalize(): void 'native-array' => true, '\Traversable' => true, ], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalizePaginator(): void diff --git a/src/JsonApi/composer.json b/src/JsonApi/composer.json index 6ab91ec6571..60e5fd25bef 100644 --- a/src/JsonApi/composer.json +++ b/src/JsonApi/composer.json @@ -21,14 +21,14 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/documentation": "^3.4 || ^4.0", "api-platform/json-schema": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/serializer": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", - "symfony/error-handler": "^6.4 || ^7.1", - "symfony/http-foundation": "^6.4 || ^7.1" + "symfony/error-handler": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0" }, "require-dev": { "phpspec/prophecy": "^1.19", @@ -59,7 +59,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/JsonLd/Action/ContextAction.php b/src/JsonLd/Action/ContextAction.php index c53f5d08774..74c144ed81b 100644 --- a/src/JsonLd/Action/ContextAction.php +++ b/src/JsonLd/Action/ContextAction.php @@ -56,6 +56,10 @@ public function __construct( */ public function __invoke(string $shortName = 'Entrypoint', ?Request $request = null): array|Response { + if (!$shortName) { + $shortName = 'Entrypoint'; + } + if (null !== $request && $this->provider && $this->processor && $this->serializer) { $operation = new Get( outputFormats: ['jsonld' => ['application/ld+json']], diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index 5cb591d0fab..a3d638f5a7b 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -185,7 +185,7 @@ private function getResourceContextWithShortname(string $resourceClass, int $ref } if (false === ($this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) { - return ['http://www.w3.org/ns/hydra/context.jsonld', $context]; + return [HYDRA_CONTEXT, $context]; } return $context; diff --git a/src/JsonLd/ContextBuilderInterface.php b/src/JsonLd/ContextBuilderInterface.php index 63c34d66c42..c90152c1f83 100644 --- a/src/JsonLd/ContextBuilderInterface.php +++ b/src/JsonLd/ContextBuilderInterface.php @@ -23,6 +23,7 @@ */ interface ContextBuilderInterface { + public const HYDRA_CONTEXT = 'http://www.w3.org/ns/hydra/context.jsonld'; public const HYDRA_NS = 'http://www.w3.org/ns/hydra/core#'; public const JSONLD_NS = 'http://www.w3.org/ns/json-ld#'; public const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; diff --git a/src/JsonLd/HydraContext.php b/src/JsonLd/HydraContext.php new file mode 100644 index 00000000000..90b6858682a --- /dev/null +++ b/src/JsonLd/HydraContext.php @@ -0,0 +1,919 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd; + +/* + * This is an autogenerated file, DO NOT MODIFY IT. + * Run the update-hydra-context.php script at the root of the project to refresh it. + */ +const HYDRA_CONTEXT = [ + '@context' => [ + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'vs' => 'http://www.w3.org/2003/06/sw-vocab-status/ns#', + 'dc' => 'http://purl.org/dc/terms/', + 'cc' => 'http://creativecommons.org/ns#', + 'schema' => 'http://schema.org/', + 'apiDocumentation' => 'hydra:apiDocumentation', + 'ApiDocumentation' => 'hydra:ApiDocumentation', + 'title' => 'hydra:title', + 'description' => 'hydra:description', + 'entrypoint' => [ + '@id' => 'hydra:entrypoint', + '@type' => '@id', + ], + 'supportedClass' => [ + '@id' => 'hydra:supportedClass', + '@type' => '@vocab', + ], + 'Class' => 'hydra:Class', + 'supportedProperty' => [ + '@id' => 'hydra:supportedProperty', + '@type' => '@id', + ], + 'SupportedProperty' => 'hydra:SupportedProperty', + 'property' => [ + '@id' => 'hydra:property', + '@type' => '@vocab', + ], + 'required' => 'hydra:required', + 'readable' => 'hydra:readable', + 'writable' => 'hydra:writable', + 'writeable' => 'hydra:writeable', + 'supportedOperation' => [ + '@id' => 'hydra:supportedOperation', + '@type' => '@id', + ], + 'Operation' => 'hydra:Operation', + 'method' => 'hydra:method', + 'expects' => [ + '@id' => 'hydra:expects', + '@type' => '@vocab', + ], + 'returns' => [ + '@id' => 'hydra:returns', + '@type' => '@vocab', + ], + 'possibleStatus' => [ + '@id' => 'hydra:possibleStatus', + '@type' => '@id', + ], + 'Status' => 'hydra:Status', + 'statusCode' => 'hydra:statusCode', + 'Error' => 'hydra:Error', + 'Resource' => 'hydra:Resource', + 'operation' => 'hydra:operation', + 'Collection' => 'hydra:Collection', + 'collection' => 'hydra:collection', + 'member' => [ + '@id' => 'hydra:member', + '@type' => '@id', + ], + 'memberAssertion' => 'hydra:memberAssertion', + 'manages' => 'hydra:manages', + 'subject' => [ + '@id' => 'hydra:subject', + '@type' => '@vocab', + ], + 'object' => [ + '@id' => 'hydra:object', + '@type' => '@vocab', + ], + 'search' => 'hydra:search', + 'freetextQuery' => 'hydra:freetextQuery', + 'view' => [ + '@id' => 'hydra:view', + '@type' => '@id', + ], + 'PartialCollectionView' => 'hydra:PartialCollectionView', + 'totalItems' => 'hydra:totalItems', + 'first' => [ + '@id' => 'hydra:first', + '@type' => '@id', + ], + 'last' => [ + '@id' => 'hydra:last', + '@type' => '@id', + ], + 'next' => [ + '@id' => 'hydra:next', + '@type' => '@id', + ], + 'previous' => [ + '@id' => 'hydra:previous', + '@type' => '@id', + ], + 'Link' => 'hydra:Link', + 'TemplatedLink' => 'hydra:TemplatedLink', + 'IriTemplate' => 'hydra:IriTemplate', + 'template' => 'hydra:template', + 'Rfc6570Template' => 'hydra:Rfc6570Template', + 'variableRepresentation' => [ + '@id' => 'hydra:variableRepresentation', + '@type' => '@vocab', + ], + 'VariableRepresentation' => 'hydra:VariableRepresentation', + 'BasicRepresentation' => 'hydra:BasicRepresentation', + 'ExplicitRepresentation' => 'hydra:ExplicitRepresentation', + 'mapping' => 'hydra:mapping', + 'IriTemplateMapping' => 'hydra:IriTemplateMapping', + 'variable' => 'hydra:variable', + 'offset' => [ + '@id' => 'hydra:offset', + '@type' => 'xsd:nonNegativeInteger', + ], + 'limit' => [ + '@id' => 'hydra:limit', + '@type' => 'xsd:nonNegativeInteger', + ], + 'pageIndex' => [ + '@id' => 'hydra:pageIndex', + '@type' => 'xsd:nonNegativeInteger', + ], + 'pageReference' => [ + '@id' => 'hydra:pageReference', + ], + 'returnsHeader' => [ + '@id' => 'hydra:returnsHeader', + '@type' => 'xsd:string', + ], + 'expectsHeader' => [ + '@id' => 'hydra:expectsHeader', + '@type' => 'xsd:string', + ], + 'HeaderSpecification' => 'hydra:HeaderSpecification', + 'headerName' => 'hydra:headerName', + 'possibleValue' => 'hydra:possibleValue', + 'closedSet' => [ + '@id' => 'hydra:possibleValue', + '@type' => 'xsd:boolean', + ], + 'name' => [ + '@id' => 'hydra:name', + '@type' => 'xsd:string', + ], + 'extension' => [ + '@id' => 'hydra:extension', + '@type' => '@id', + ], + 'isDefinedBy' => [ + '@id' => 'rdfs:isDefinedBy', + '@type' => '@id', + ], + 'defines' => [ + '@reverse' => 'rdfs:isDefinedBy', + ], + 'comment' => 'rdfs:comment', + 'label' => 'rdfs:label', + 'preferredPrefix' => 'http://purl.org/vocab/vann/preferredNamespacePrefix', + 'cc:license' => [ + '@type' => '@id', + ], + 'cc:attributionURL' => [ + '@type' => '@id', + ], + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@vocab', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@vocab', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@vocab', + ], + 'subPropertyOf' => [ + '@id' => 'rdfs:subPropertyOf', + '@type' => '@vocab', + ], + 'seeAlso' => [ + '@id' => 'rdfs:seeAlso', + '@type' => '@id', + ], + 'domainIncludes' => [ + '@id' => 'schema:domainIncludes', + '@type' => '@id', + ], + 'rangeIncludes' => [ + '@id' => 'schema:rangeIncludes', + '@type' => '@id', + ], + ], + '@id' => 'http://www.w3.org/ns/hydra/core', + '@type' => 'owl:Ontology', + 'label' => 'The Hydra Core Vocabulary', + 'comment' => 'A lightweight vocabulary for hypermedia-driven Web APIs', + 'seeAlso' => 'https://www.hydra-cg.com/spec/latest/core/', + 'preferredPrefix' => 'hydra', + 'dc:description' => 'The Hydra Core Vocabulary is a lightweight vocabulary to create hypermedia-driven Web APIs. By specifying a number of concepts commonly used in Web APIs it enables the creation of generic API clients.', + 'dc:rights' => 'Copyright © 2012-2014 the Contributors to the Hydra Core Vocabulary Specification', + 'dc:publisher' => 'Hydra W3C Community Group', + 'cc:license' => 'http://creativecommons.org/licenses/by/4.0/', + 'cc:attributionName' => 'Hydra W3C Community Group', + 'cc:attributionURL' => 'http://www.hydra-cg.com/', + 'defines' => [ + 0 => [ + '@id' => 'hydra:Resource', + '@type' => 'hydra:Class', + 'label' => 'Hydra Resource', + 'comment' => 'The class of dereferenceable resources by means a client can attempt to dereference; however, the received responses should still be verified.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 1 => [ + '@id' => 'hydra:Class', + '@type' => [ + 0 => 'hydra:Resource', + 1 => 'rdfs:Class', + ], + 'subClassOf' => [ + 0 => 'rdfs:Class', + ], + 'label' => 'Hydra Class', + 'comment' => 'The class of Hydra classes.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 2 => [ + '@id' => 'hydra:Link', + '@type' => 'hydra:Class', + 'subClassOf' => [ + 0 => 'hydra:Resource', + 1 => 'rdf:Property', + ], + 'label' => 'Link', + 'comment' => 'The class of properties representing links.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 3 => [ + '@id' => 'hydra:apiDocumentation', + '@type' => 'hydra:Link', + 'label' => 'apiDocumentation', + 'comment' => 'A link to the API documentation', + 'range' => 'hydra:ApiDocumentation', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 4 => [ + '@id' => 'hydra:ApiDocumentation', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'ApiDocumentation', + 'comment' => 'The Hydra API documentation class', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 5 => [ + '@id' => 'hydra:entrypoint', + '@type' => 'hydra:Link', + 'label' => 'entrypoint', + 'comment' => 'A link to main entry point of the Web API', + 'domain' => 'hydra:ApiDocumentation', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 6 => [ + '@id' => 'hydra:supportedClass', + '@type' => 'hydra:Link', + 'label' => 'supported classes', + 'comment' => 'A class known to be supported by the Web API', + 'domain' => 'hydra:ApiDocumentation', + 'range' => 'rdfs:Class', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 7 => [ + '@id' => 'hydra:possibleStatus', + '@type' => 'hydra:Link', + 'label' => 'possible status', + 'comment' => 'A status that might be returned by the Web API (other statuses should be expected and properly handled as well)', + 'range' => 'hydra:Status', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Operation', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 8 => [ + '@id' => 'hydra:supportedProperty', + '@type' => 'hydra:Link', + 'label' => 'supported properties', + 'comment' => 'The properties known to be supported by a Hydra class', + 'domain' => 'rdfs:Class', + 'range' => 'hydra:SupportedProperty', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 9 => [ + '@id' => 'hydra:SupportedProperty', + '@type' => 'hydra:Class', + 'label' => 'Supported Property', + 'comment' => 'A property known to be supported by a Hydra class.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 10 => [ + '@id' => 'hydra:property', + '@type' => 'rdf:Property', + 'label' => 'property', + 'comment' => 'A property', + 'range' => 'rdf:Property', + 'domainIncludes' => [ + 0 => 'hydra:SupportedProperty', + 1 => 'hydra:IriTemplateMapping', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 11 => [ + '@id' => 'hydra:required', + '@type' => 'rdf:Property', + 'label' => 'required', + 'comment' => 'True if the property is required, false otherwise.', + 'range' => 'xsd:boolean', + 'domainIncludes' => [ + 0 => 'hydra:SupportedProperty', + 1 => 'hydra:IriTemplateMapping', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 12 => [ + '@id' => 'hydra:readable', + '@type' => 'rdf:Property', + 'label' => 'readable', + 'comment' => 'True if the client can retrieve the property\'s value, false otherwise.', + 'domain' => 'hydra:SupportedProperty', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 13 => [ + '@id' => 'hydra:writable', + '@type' => 'rdf:Property', + 'label' => 'writable', + 'comment' => 'True if the client can change the property\'s value, false otherwise.', + 'domain' => 'hydra:SupportedProperty', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 14 => [ + '@id' => 'hydra:writeable', + 'subPropertyOf' => 'hydra:writable', + 'label' => 'writable', + 'comment' => 'This property is left for compatibility purposes and hydra:writable should be used instead.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'archaic', + ], + 15 => [ + '@id' => 'hydra:supportedOperation', + '@type' => 'hydra:Link', + 'label' => 'supported operation', + 'comment' => 'An operation supported by instances of the specific Hydra class, or the target of the Hydra link, or IRI template.', + 'range' => 'hydra:Operation', + 'domainIncludes' => [ + 0 => 'rdfs:Class', + 1 => 'hydra:Class', + 2 => 'hydra:Link', + 3 => 'hydra:TemplatedLink', + 4 => 'hydra:SupportedProperty', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 16 => [ + '@id' => 'hydra:operation', + '@type' => 'hydra:Link', + 'label' => 'operation', + 'comment' => 'An operation supported by the Hydra resource', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Operation', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 17 => [ + '@id' => 'hydra:Operation', + '@type' => 'hydra:Class', + 'label' => 'Operation', + 'comment' => 'An operation.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 18 => [ + '@id' => 'hydra:method', + '@type' => 'rdf:Property', + 'label' => 'method', + 'comment' => 'The HTTP method.', + 'domain' => 'hydra:Operation', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 19 => [ + '@id' => 'hydra:expects', + '@type' => 'hydra:Link', + 'label' => 'expects', + 'comment' => 'The information expected by the Web API.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'rdfs:Resource', + 1 => 'hydra:Resource', + 2 => 'rdfs:Class', + 3 => 'hydra:Class', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 20 => [ + '@id' => 'hydra:returns', + '@type' => 'hydra:Link', + 'label' => 'returns', + 'comment' => 'The information returned by the Web API on success', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'rdfs:Resource', + 1 => 'hydra:Resource', + 2 => 'rdfs:Class', + 3 => 'hydra:Class', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 21 => [ + '@id' => 'hydra:Status', + '@type' => 'hydra:Class', + 'label' => 'Status code description', + 'comment' => 'Additional information about a status code that might be returned.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 22 => [ + '@id' => 'hydra:statusCode', + '@type' => 'rdf:Property', + 'label' => 'status code', + 'comment' => 'The HTTP status code. Please note it may happen this value will be different to actual status code received.', + 'domain' => 'hydra:Status', + 'range' => 'xsd:integer', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 23 => [ + '@id' => 'hydra:title', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'rdfs:label', + 'label' => 'title', + 'comment' => 'A title, often used along with a description.', + 'range' => 'xsd:string', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Status', + 2 => 'hydra:Class', + 3 => 'hydra:SupportedProperty', + 4 => 'hydra:Operation', + 5 => 'hydra:Link', + 6 => 'hydra:TemplatedLink', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 24 => [ + '@id' => 'hydra:description', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'rdfs:comment', + 'label' => 'description', + 'comment' => 'A description.', + 'range' => 'xsd:string', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Status', + 2 => 'hydra:Class', + 3 => 'hydra:SupportedProperty', + 4 => 'hydra:Operation', + 5 => 'hydra:Link', + 6 => 'hydra:TemplatedLink', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 25 => [ + '@id' => 'hydra:Error', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Status', + 'label' => 'Error', + 'comment' => 'A runtime error, used to report information beyond the returned status code.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 26 => [ + '@id' => 'hydra:Collection', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Collection', + 'comment' => 'A collection holding references to a number of related resources.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 27 => [ + '@id' => 'hydra:collection', + '@type' => 'hydra:Link', + 'label' => 'collection', + 'comment' => 'Collections somehow related to this resource.', + 'range' => 'hydra:Collection', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 28 => [ + '@id' => 'hydra:memberAssertion', + 'label' => 'member assertion', + 'comment' => 'Semantics of each member provided by the collection.', + 'domain' => 'hydra:Collection', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 29 => [ + '@id' => 'hydra:manages', + 'subPropertyOf' => 'hydra:memberAssertion', + 'label' => 'manages', + 'comment' => 'This predicate is left for compatibility purposes and hydra:memberAssertion should be used instead.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'archaic', + ], + 30 => [ + '@id' => 'hydra:subject', + 'label' => 'subject', + 'comment' => 'The subject.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 31 => [ + '@id' => 'hydra:object', + 'label' => 'object', + 'comment' => 'The object.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 32 => [ + '@id' => 'hydra:member', + '@type' => 'hydra:Link', + 'label' => 'member', + 'comment' => 'A member of the collection', + 'domain' => 'hydra:Collection', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 33 => [ + '@id' => 'hydra:view', + '@type' => 'hydra:Link', + 'label' => 'view', + 'comment' => 'A specific view of a resource.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 34 => [ + '@id' => 'hydra:PartialCollectionView', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'PartialCollectionView', + 'comment' => 'A PartialCollectionView describes a partial view of a Collection. Multiple PartialCollectionViews can be connected with the the next/previous properties to allow a client to retrieve all members of the collection.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 35 => [ + '@id' => 'hydra:totalItems', + '@type' => 'rdf:Property', + 'label' => 'total items', + 'comment' => 'The total number of items referenced by a collection.', + 'domain' => 'hydra:Collection', + 'range' => 'xsd:integer', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 36 => [ + '@id' => 'hydra:first', + '@type' => 'hydra:Link', + 'label' => 'first', + 'comment' => 'The first resource of an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 37 => [ + '@id' => 'hydra:last', + '@type' => 'hydra:Link', + 'label' => 'last', + 'comment' => 'The last resource of an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 38 => [ + '@id' => 'hydra:next', + '@type' => 'hydra:Link', + 'label' => 'next', + 'comment' => 'The resource following the current instance in an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 39 => [ + '@id' => 'hydra:previous', + '@type' => 'hydra:Link', + 'label' => 'previous', + 'comment' => 'The resource preceding the current instance in an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 40 => [ + '@id' => 'hydra:search', + '@type' => 'hydra:TemplatedLink', + 'label' => 'search', + 'comment' => 'A IRI template that can be used to query a collection.', + 'range' => 'hydra:IriTemplate', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 41 => [ + '@id' => 'hydra:freetextQuery', + '@type' => 'rdf:Property', + 'label' => 'freetext query', + 'comment' => 'A property representing a freetext query.', + 'range' => 'xsd:string', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 42 => [ + '@id' => 'hydra:TemplatedLink', + '@type' => 'hydra:Class', + 'subClassOf' => [ + 0 => 'hydra:Resource', + 1 => 'rdf:Property', + ], + 'label' => 'Templated Link', + 'comment' => 'A templated link.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 43 => [ + '@id' => 'hydra:IriTemplate', + '@type' => 'hydra:Class', + 'label' => 'IRI Template', + 'comment' => 'The class of IRI templates.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 44 => [ + '@id' => 'hydra:template', + '@type' => 'rdf:Property', + 'label' => 'template', + 'comment' => 'A templated string with placeholders. The literal\'s datatype indicates the template syntax; if not specified, hydra:Rfc6570Template is assumed.', + 'seeAlso' => 'hydra:Rfc6570Template', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:Rfc6570Template', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 45 => [ + '@id' => 'hydra:Rfc6570Template', + '@type' => 'rdfs:Datatype', + 'label' => 'RFC6570 IRI template', + 'comment' => 'An IRI template as defined by RFC6570.', + 'seeAlso' => 'http://tools.ietf.org/html/rfc6570', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 46 => [ + '@id' => 'hydra:variableRepresentation', + '@type' => 'rdf:Property', + 'label' => 'variable representation', + 'comment' => 'The representation format to use when expanding the IRI template.', + 'range' => 'hydra:VariableRepresentation', + 'domain' => 'hydra:IriTemplateMapping', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 47 => [ + '@id' => 'hydra:VariableRepresentation', + '@type' => 'hydra:Class', + 'label' => 'VariableRepresentation', + 'comment' => 'A representation specifies how to serialize variable values into strings.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 48 => [ + '@id' => 'hydra:BasicRepresentation', + '@type' => 'hydra:VariableRepresentation', + 'label' => 'BasicRepresentation', + 'comment' => 'A representation that serializes just the lexical form of a variable value, but omits language and type information.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 49 => [ + '@id' => 'hydra:ExplicitRepresentation', + '@type' => 'hydra:VariableRepresentation', + 'label' => 'ExplicitRepresentation', + 'comment' => 'A representation that serializes a variable value including its language and type information and thus differentiating between IRIs and literals.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 50 => [ + '@id' => 'hydra:mapping', + '@type' => 'rdf:Property', + 'label' => 'mapping', + 'comment' => 'A variable-to-property mapping of the IRI template.', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:IriTemplateMapping', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 51 => [ + '@id' => 'hydra:IriTemplateMapping', + '@type' => 'hydra:Class', + 'label' => 'IriTemplateMapping', + 'comment' => 'A mapping from an IRI template variable to a property.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 52 => [ + '@id' => 'hydra:variable', + '@type' => 'rdf:Property', + 'label' => 'variable', + 'comment' => 'An IRI template variable', + 'domain' => 'hydra:IriTemplateMapping', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 53 => [ + '@id' => 'hydra:resolveRelativeUsing', + '@type' => 'rdf:Property', + 'label' => 'relative Uri resolution', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:BaseUriSource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 54 => [ + '@id' => 'hydra:BaseUriSource', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Base Uri source', + 'comment' => 'Provides a base abstract for base Uri source for Iri template resolution.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 55 => [ + '@id' => 'hydra:Rfc3986', + '@type' => 'hydra:BaseUriSource', + 'label' => 'RFC 3986 based', + 'comment' => 'States that the base Uri should be established using RFC 3986 reference resolution algorithm specified in section 5.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 56 => [ + '@id' => 'hydra:LinkContext', + '@type' => 'hydra:BaseUriSource', + 'label' => 'Link context', + 'comment' => 'States that the link\'s context IRI, as defined in RFC 5988, should be used as the base Uri', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 57 => [ + '@id' => 'hydra:offset', + '@type' => 'rdf:Property', + 'label' => 'skip', + 'comment' => 'Instructs to skip N elements of the set.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 58 => [ + '@id' => 'hydra:limit', + '@type' => 'rdf:Property', + 'label' => 'take', + 'comment' => 'Instructs to limit set only to N elements.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 59 => [ + '@id' => 'hydra:pageIndex', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'hydra:pageReference', + 'label' => 'page index', + 'comment' => 'Instructs to provide a specific page of the collection at a given index.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 60 => [ + '@id' => 'hydra:pageReference', + '@type' => 'rdf:Property', + 'label' => 'page reference', + 'comment' => 'Instructs to provide a specific page reference of the collection.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 61 => [ + '@id' => 'hydra:returnsHeader', + '@type' => 'rdf:Property', + 'label' => 'returns header', + 'comment' => 'Name of the header returned by the operation.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'xsd:string', + 1 => 'hydra:HeaderSpecification', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 62 => [ + '@id' => 'hydra:expectsHeader', + '@type' => 'rdf:Property', + 'label' => 'expects header', + 'comment' => 'Specification of the header expected by the operation.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'xsd:string', + 1 => 'hydra:HeaderSpecification', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 63 => [ + '@id' => 'hydra:HeaderSpecification', + '@type' => 'rdfs:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Header specification', + 'comment' => 'Specifies a possible either expected or returned header values', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 64 => [ + '@id' => 'hydra:headerName', + '@type' => 'rdf:Property', + 'label' => 'header name', + 'comment' => 'Name of the header.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 65 => [ + '@id' => 'hydra:possibleValue', + '@type' => 'rdf:Property', + 'label' => 'possible header value', + 'comment' => 'Possible value of the header.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:string', + 'vs:term_status' => 'testing', + ], + 66 => [ + '@id' => 'hydra:closedSet', + '@type' => 'rdf:Property', + 'label' => 'closed set', + 'comment' => 'Determines whether the provided set of header values is closed or not.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 67 => [ + '@id' => 'hydra:extension', + '@type' => 'rdf:Property', + 'label' => 'extension', + 'comment' => 'Hint on what kind of extensions are in use.', + 'domain' => 'hydra:ApiDocumentation', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + ], +]; diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php index d1b27e9d3f0..ce8dde4c9bb 100644 --- a/src/JsonLd/Serializer/ErrorNormalizer.php +++ b/src/JsonLd/Serializer/ErrorNormalizer.php @@ -14,7 +14,6 @@ namespace ApiPlatform\JsonLd\Serializer; use ApiPlatform\State\ApiResource\Error; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as SymfonyValidationException; use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -53,7 +52,7 @@ public function normalize(mixed $object, ?string $format = null, array $context public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { return $this->inner->supportsNormalization($data, $format, $context) - && (is_a($data, Error::class) || is_a($data, ValidationException::class) || is_a($data, SymfonyValidationException::class)); + && (is_a($data, Error::class) || is_a($data, ValidationException::class)); } public function getSupportedTypes(?string $format): array diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index a64b3d7f8a4..8d6e2aefb27 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -13,8 +13,6 @@ namespace ApiPlatform\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; use ApiPlatform\Metadata\Exception\ItemNotFoundException; @@ -72,7 +70,7 @@ final class ItemNormalizer extends AbstractItemNormalizer '@vocab', ]; - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); } @@ -177,7 +175,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, } catch (ItemNotFoundException $e) { $operation = $context['operation'] ?? null; - if (!('PUT' === $operation?->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false))) { + if (!('PUT' === $operation?->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true))) { throw $e; } } diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php index edae4b21d10..2e0cef8a402 100644 --- a/src/JsonLd/Serializer/JsonLdContextTrait.php +++ b/src/JsonLd/Serializer/JsonLdContextTrait.php @@ -15,7 +15,6 @@ use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; -use ApiPlatform\Metadata\Error; /** * Creates and manipulates the Serializer context. @@ -43,10 +42,6 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin return $data; } - if (($operation = $context['operation'] ?? null) && ($operation->getExtraProperties()['rfc_7807_compliant_errors'] ?? false) && $operation instanceof Error) { - return $data; - } - $data['@context'] = $contextBuilder->getResourceContextUri($resourceClass); return $data; diff --git a/src/JsonLd/Serializer/ObjectNormalizer.php b/src/JsonLd/Serializer/ObjectNormalizer.php index c85bc3daf51..a7816eee5ec 100644 --- a/src/JsonLd/Serializer/ObjectNormalizer.php +++ b/src/JsonLd/Serializer/ObjectNormalizer.php @@ -13,26 +13,22 @@ namespace ApiPlatform\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON-LD metadata when appropriate, but otherwise just * passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use JsonLdContextTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private AnonymousContextBuilderInterface $anonymousContextBuilder) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private AnonymousContextBuilderInterface $anonymousContextBuilder) { } @@ -46,30 +42,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/JsonLd/composer.json b/src/JsonLd/composer.json index 53f6887fbd0..7b27cfca087 100644 --- a/src/JsonLd/composer.json +++ b/src/JsonLd/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/state": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/serializer": "^3.4 || ^4.0" @@ -32,6 +32,9 @@ "psr-4": { "ApiPlatform\\JsonLd\\": "" }, + "files": [ + "./HydraContext.php" + ], "exclude-from-classmap": [ "/Tests/" ] @@ -52,7 +55,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", @@ -61,5 +64,8 @@ }, "scripts": { "test": "./vendor/bin/phpunit" + }, + "require-dev": { + "phpunit/phpunit": "^11.2" } } diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 9c6c6a38a35..85b43e64ba3 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -33,18 +33,14 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { use ResourceMetadataTrait; - private ?TypeFactoryInterface $typeFactory = null; + private ?SchemaFactoryInterface $schemaFactory = null; // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link'; public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { - if ($typeFactory) { - trigger_deprecation('api-platform/core', '3.4', \sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); - $this->typeFactory = $typeFactory; - } if (!$definitionNameFactory) { $this->definitionNameFactory = new DefinitionNameFactory($this->distinctFormats); } @@ -208,12 +204,6 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $subSchema = new Schema($version); $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema - // TODO: in 3.3 add trigger_deprecation() as type factories are not used anymore, we moved this logic to SchemaPropertyMetadataFactory so that it gets cached - if ($typeFromFactory = $this->typeFactory?->getType($type, 'jsonschema', $propertyMetadata->isReadableLink(), $serializerContext)) { - $propertySchema = $typeFromFactory; - break; - } - $isCollection = $type->isCollection(); if ($isCollection) { $valueType = $type->getCollectionValueTypes()[0] ?? null; diff --git a/src/JsonSchema/Tests/Fixtures/NotAResource.php b/src/JsonSchema/Tests/Fixtures/NotAResource.php index f6a5519c68b..263c72b8613 100644 --- a/src/JsonSchema/Tests/Fixtures/NotAResource.php +++ b/src/JsonSchema/Tests/Fixtures/NotAResource.php @@ -13,7 +13,7 @@ namespace ApiPlatform\JsonSchema\Tests\Fixtures; -use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Attribute\Groups; /** * This class is not mapped as an API resource. @@ -23,13 +23,9 @@ class NotAResource { public function __construct( - /** - * @Groups("contain_non_resource") - */ + #[Groups('contain_non_resource')] private $foo, - /** - * @Groups("contain_non_resource") - */ + #[Groups('contain_non_resource')] private $bar, ) { } diff --git a/src/JsonSchema/Tests/SchemaFactoryTest.php b/src/JsonSchema/Tests/SchemaFactoryTest.php index bf3bd56ae46..da38fc6474d 100644 --- a/src/JsonSchema/Tests/SchemaFactoryTest.php +++ b/src/JsonSchema/Tests/SchemaFactoryTest.php @@ -78,7 +78,6 @@ public function testBuildSchemaForNonResourceClass(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -154,7 +153,6 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -237,7 +235,6 @@ public function testBuildSchemaWithSerializerGroups(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -295,7 +292,6 @@ public function testBuildSchemaForAssociativeArray(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), diff --git a/src/JsonSchema/Tests/SchemaTest.php b/src/JsonSchema/Tests/SchemaTest.php index 73175d0995a..d60ab82a6ba 100644 --- a/src/JsonSchema/Tests/SchemaTest.php +++ b/src/JsonSchema/Tests/SchemaTest.php @@ -18,9 +18,7 @@ class SchemaTest extends TestCase { - /** - * @dataProvider versionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] public function testJsonSchemaVersion(string $version, string $ref): void { $schema = new Schema($version); @@ -31,9 +29,7 @@ public function testJsonSchemaVersion(string $version, string $ref): void $this->assertSame('Foo', $schema->getRootDefinitionKey()); } - /** - * @dataProvider versionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] public function testCollectionJsonSchemaVersion(string $version, string $ref): void { $schema = new Schema($version); @@ -62,9 +58,7 @@ public function testContainsJsonSchemaVersion(): void $this->assertSame('http://json-schema.org/draft-07/schema#', $schema['$schema']); } - /** - * @dataProvider definitionsDataProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('definitionsDataProvider')] public function testDefinitions(string $version, array $baseDefinitions): void { $schema = new Schema($version); diff --git a/src/JsonSchema/Tests/TypeFactoryTest.php b/src/JsonSchema/Tests/TypeFactoryTest.php deleted file mode 100644 index e1a5a0cd8cc..00000000000 --- a/src/JsonSchema/Tests/TypeFactoryTest.php +++ /dev/null @@ -1,472 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema\Tests; - -use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; -use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GamePlayMode; -use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GenderTypeEnum; -use ApiPlatform\JsonSchema\TypeFactory; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; - -class TypeFactoryTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider typeProvider - */ - public function testGetType(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_OPENAPI))); - } - - public static function typeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['nullable' => true, 'type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['nullable' => true, 'type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['nullable' => true, 'type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['nullable' => true, 'type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['nullable' => true, 'type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['nullable' => true, 'type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/', 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable' => [ - ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values' => [ - [ - 'type' => 'array', - 'items' => [ - 'nullable' => true, - 'type' => 'string', - ], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object' => [ - [ - 'nullable' => true, - 'type' => 'object', - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered' => [ - [ - 'type' => 'object', - 'additionalProperties' => [ - 'nullable' => true, - 'type' => 'integer', - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values' => [ - [ - 'nullable' => true, - 'type' => 'object', - 'additionalProperties' => [ - 'nullable' => true, - 'type' => 'integer', - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - /** - * @dataProvider jsonSchemaTypeProvider - */ - public function testGetTypeWithJsonSchemaSyntax(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_JSON_SCHEMA))); - } - - public static function jsonSchemaTypeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['type' => ['integer', 'null']], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['type' => ['number', 'null']], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['type' => ['boolean', 'null']], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['type' => ['string', 'null'], 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['type' => ['string', 'null'], 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => ['string', 'null'], 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable' => [ - ['type' => ['array', 'null'], 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values' => [ - [ - 'type' => 'array', - 'items' => [ - 'type' => ['string', 'null'], - ], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object' => [ - [ - 'type' => ['object', 'null'], - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered' => [ - [ - 'type' => 'object', - 'additionalProperties' => [ - 'type' => ['integer', 'null'], - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values' => [ - [ - 'type' => ['object', 'null'], - 'additionalProperties' => [ - 'type' => ['integer', 'null'], - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - /** @dataProvider openAPIV2TypeProvider */ - public function testGetTypeWithOpenAPIV2Syntax(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_SWAGGER))); - } - - public static function openAPIV2TypeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ - ['type' => 'array', 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values, but ignored in OpenAPI V2' => [ - [ - 'type' => 'array', - 'items' => ['type' => 'string'], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'integer'], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'integer'], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - public function testGetClassType(): void - { - $schemaFactoryProphecy = $this->prophesize(SchemaFactoryInterface::class); - - $schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, Argument::type(Schema::class), Argument::type('array'), false)->will(function (array $args) { - $args[4]['$ref'] = 'ref'; - - return $args[4]; - }); - - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactoryProphecy->reveal()); - - $this->assertEquals(['$ref' => 'ref'], $typeFactory->getType(new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class), 'jsonld', true, ['foo' => 'bar'], new Schema())); - } - - /** @dataProvider classTypeWithNullabilityDataProvider */ - public function testGetClassTypeWithNullability(array $expected, callable $schemaFactoryFactory, Schema $schema): void - { - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactoryFactory($this)); - - self::assertEquals( - $expected, - $typeFactory->getType(new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class), 'jsonld', true, ['foo' => 'bar'], $schema) - ); - } - - public static function classTypeWithNullabilityDataProvider(): iterable - { - $schema = new Schema(); - $schemaFactoryFactory = fn (self $that): SchemaFactoryInterface => $that->createSchemaFactoryMock($schema); - - yield 'JSON-Schema version' => [ - [ - 'anyOf' => [ - ['$ref' => 'the-ref-name'], - ['type' => 'null'], - ], - ], - $schemaFactoryFactory, - $schema, - ]; - - $schema = new Schema(Schema::VERSION_OPENAPI); - $schemaFactoryFactory = fn (self $that): SchemaFactoryInterface => $that->createSchemaFactoryMock($schema); - - yield 'OpenAPI < 3.1 version' => [ - [ - 'anyOf' => [ - ['$ref' => 'the-ref-name'], - ], - 'nullable' => true, - ], - $schemaFactoryFactory, - $schema, - ]; - } - - private function createSchemaFactoryMock(Schema $schema): SchemaFactoryInterface - { - $schemaFactory = $this->createMock(SchemaFactoryInterface::class); - - $schemaFactory - ->method('buildSchema') - ->willReturnCallback(static function () use ($schema): Schema { - $schema['$ref'] = 'the-ref-name'; - $schema['description'] = 'more stuff here'; - - return $schema; - }); - - return $schemaFactory; - } -} diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php deleted file mode 100644 index 3921913e82e..00000000000 --- a/src/JsonSchema/TypeFactory.php +++ /dev/null @@ -1,207 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema; - -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; -use Ramsey\Uuid\UuidInterface; -use Symfony\Component\PropertyInfo\Type; -use Symfony\Component\Uid\Ulid; -use Symfony\Component\Uid\Uuid; - -/** - * {@inheritdoc} - * - * @deprecated since 3.3 https://github.com/api-platform/core/pull/5470 - * - * @author Kévin Dunglas - */ -final class TypeFactory implements TypeFactoryInterface -{ - use ResourceClassInfoTrait; - - private ?SchemaFactoryInterface $schemaFactory = null; - - public function __construct(?ResourceClassResolverInterface $resourceClassResolver = null) - { - $this->resourceClassResolver = $resourceClassResolver; - } - - public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void - { - $this->schemaFactory = $schemaFactory; - } - - /** - * {@inheritdoc} - */ - public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array - { - if ('jsonschema' === $format) { - return []; - } - - // TODO: OpenApiFactory uses this to compute filter types - if ($type->isCollection()) { - $keyType = $type->getCollectionKeyTypes()[0] ?? null; - $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false); - - if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) { - return $this->addNullabilityToTypeDefinition([ - 'type' => 'object', - 'additionalProperties' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema), - ], $type, $schema); - } - - return $this->addNullabilityToTypeDefinition([ - 'type' => 'array', - 'items' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema), - ], $type, $schema); - } - - return $this->addNullabilityToTypeDefinition($this->makeBasicType($type, $format, $readableLink, $serializerContext, $schema), $type, $schema); - } - - private function makeBasicType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array - { - return match ($type->getBuiltinType()) { - Type::BUILTIN_TYPE_INT => ['type' => 'integer'], - Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], - Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], - Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $format, $readableLink, $serializerContext, $schema), - default => ['type' => 'string'], - }; - } - - /** - * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. - */ - private function getClassType(?string $className, bool $nullable, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array - { - if (null === $className) { - return ['type' => 'string']; - } - - if (is_a($className, \DateTimeInterface::class, true)) { - return [ - 'type' => 'string', - 'format' => 'date-time', - ]; - } - if (is_a($className, \DateInterval::class, true)) { - return [ - 'type' => 'string', - 'format' => 'duration', - ]; - } - if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) { - return [ - 'type' => 'string', - 'format' => 'uuid', - ]; - } - if (is_a($className, Ulid::class, true)) { - return [ - 'type' => 'string', - 'format' => 'ulid', - ]; - } - if (is_a($className, \SplFileInfo::class, true)) { - return [ - 'type' => 'string', - 'format' => 'binary', - ]; - } - if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) { - $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); - - $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer'; - - if ($nullable) { - $enumCases[] = null; - } - - return [ - 'type' => $type, - 'enum' => $enumCases, - ]; - } - - // Skip if $schema is null (filters only support basic types) - if (null === $schema) { - return ['type' => 'string']; - } - - if (true !== $readableLink && $this->isResourceClass($className)) { - return [ - 'type' => 'string', - 'format' => 'iri-reference', - 'example' => 'https://example.com/', - ]; - } - - $version = $schema->getVersion(); - - $subSchema = new Schema($version); - $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema - - if (null === $this->schemaFactory) { - throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.'); - } - - $serializerContext += [SchemaFactory::FORCE_SUBSCHEMA => true]; - $subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, $subSchema, $serializerContext, false); - - return ['$ref' => $subSchema['$ref']]; - } - - /** - * @param array $jsonSchema - * - * @return array - */ - private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type, ?Schema $schema): array - { - if ($schema && Schema::VERSION_SWAGGER === $schema->getVersion()) { - return $jsonSchema; - } - - if (!$type->isNullable()) { - return $jsonSchema; - } - - if (\array_key_exists('$ref', $jsonSchema)) { - $typeDefinition = ['anyOf' => [$jsonSchema]]; - - if ($schema && Schema::VERSION_JSON_SCHEMA === $schema->getVersion()) { - $typeDefinition['anyOf'][] = ['type' => 'null']; - } else { - // OpenAPI < 3.1 - $typeDefinition['nullable'] = true; - } - - return $typeDefinition; - } - - if ($schema && Schema::VERSION_JSON_SCHEMA === $schema->getVersion()) { - return [...$jsonSchema, ...[ - 'type' => \is_array($jsonSchema['type']) - ? array_merge($jsonSchema['type'], ['null']) - : [$jsonSchema['type'], 'null'], - ]]; - } - - return [...$jsonSchema, ...['nullable' => true]]; - } -} diff --git a/src/JsonSchema/TypeFactoryInterface.php b/src/JsonSchema/TypeFactoryInterface.php deleted file mode 100644 index 70a551fda16..00000000000 --- a/src/JsonSchema/TypeFactoryInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema; - -use Symfony\Component\PropertyInfo\Type; - -/** - * Factory for creating the JSON Schema document which specifies the data type corresponding to a PHP type. - * - * @deprecated - * - * @author Kévin Dunglas - */ -interface TypeFactoryInterface -{ - /** - * Gets the JSON Schema document which specifies the data type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided. - */ - public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array; -} diff --git a/src/JsonSchema/composer.json b/src/JsonSchema/composer.json index dab8560e16e..0695729b5eb 100644 --- a/src/JsonSchema/composer.json +++ b/src/JsonSchema/composer.json @@ -24,17 +24,16 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "symfony/console": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -60,7 +59,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/JsonSchema/phpunit.xml.dist b/src/JsonSchema/phpunit.xml.dist index b33b9e34a4a..3e1f168b5e2 100644 --- a/src/JsonSchema/phpunit.xml.dist +++ b/src/JsonSchema/phpunit.xml.dist @@ -1,31 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/ParameterValidator/.gitattributes b/src/Laravel/.gitattributes similarity index 54% rename from src/ParameterValidator/.gitattributes rename to src/Laravel/.gitattributes index 801f2080d71..af1bba3d5e4 100644 --- a/src/ParameterValidator/.gitattributes +++ b/src/Laravel/.gitattributes @@ -1,5 +1,8 @@ -/.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/Tests export-ignore +/phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore +/CONTRIBUTING.md +/testbench.yaml +/Tests export-ignore +/workbench export-ignore diff --git a/src/Laravel/.github/workflows/close_pr.yml b/src/Laravel/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Laravel/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/ParameterValidator/.gitignore b/src/Laravel/.gitignore similarity index 100% rename from src/ParameterValidator/.gitignore rename to src/Laravel/.gitignore diff --git a/src/Laravel/ApiPlatformMiddleware.php b/src/Laravel/ApiPlatformMiddleware.php new file mode 100644 index 00000000000..d5322ca44fd --- /dev/null +++ b/src/Laravel/ApiPlatformMiddleware.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; + +class ApiPlatformMiddleware +{ + public function __construct( + protected OperationMetadataFactory $operationMetadataFactory, + ) { + } + + /** + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + */ + public function handle(Request $request, \Closure $next, ?string $operationName = null): Response + { + $operation = null; + if ($operationName) { + $request->attributes->set('_api_operation', $operation = $this->operationMetadataFactory->create($operationName)); + } + + if (!($format = $request->route('_format')) && $operation instanceof HttpOperation && str_ends_with($operation->getUriTemplate(), '{._format}')) { + $matches = []; + if (preg_match('/\.[a-zA-Z]+$/', $request->getPathInfo(), $matches)) { + $format = $matches[0]; + } + } + + $request->attributes->set('_format', $format ? substr($format, 1, \strlen($format) - 1) : ''); + + return $next($request); + } +} diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php new file mode 100644 index 00000000000..d30598bfe53 --- /dev/null +++ b/src/Laravel/ApiPlatformProvider.php @@ -0,0 +1,1421 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use ApiPlatform\GraphQl\Error\ErrorHandler as GraphQlErrorHandler; +use ApiPlatform\GraphQl\Error\ErrorHandlerInterface; +use ApiPlatform\GraphQl\Executor; +use ApiPlatform\GraphQl\ExecutorInterface; +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\GraphQl\Resolver\ResourceFieldResolver; +use ApiPlatform\GraphQl\Serializer\Exception\ErrorNormalizer as GraphQlErrorNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\HttpExceptionNormalizer as GraphQlHttpExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\RuntimeExceptionNormalizer as GraphQlRuntimeExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\ValidationExceptionNormalizer as GraphQlValidationExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\ItemNormalizer as GraphQlItemNormalizer; +use ApiPlatform\GraphQl\Serializer\ObjectNormalizer as GraphQlObjectNormalizer; +use ApiPlatform\GraphQl\Serializer\SerializerContextBuilder as GraphQlSerializerContextBuilder; +use ApiPlatform\GraphQl\State\Processor\NormalizeProcessor; +use ApiPlatform\GraphQl\State\Provider\DenormalizeProvider as GraphQlDenormalizeProvider; +use ApiPlatform\GraphQl\State\Provider\ReadProvider as GraphQlReadProvider; +use ApiPlatform\GraphQl\State\Provider\ResolverProvider; +use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilder; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; +use ApiPlatform\GraphQl\Type\SchemaBuilder; +use ApiPlatform\GraphQl\Type\SchemaBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilder; +use ApiPlatform\GraphQl\Type\TypeConverter; +use ApiPlatform\GraphQl\Type\TypeConverterInterface; +use ApiPlatform\GraphQl\Type\TypesContainer; +use ApiPlatform\GraphQl\Type\TypesContainerInterface; +use ApiPlatform\GraphQl\Type\TypesFactory; +use ApiPlatform\GraphQl\Type\TypesFactoryInterface; +use ApiPlatform\Hal\Serializer\CollectionNormalizer as HalCollectionNormalizer; +use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer; +use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer; +use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer; +use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; +use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer; +use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer; +use ApiPlatform\Hydra\Serializer\DocumentationNormalizer as HydraDocumentationNormalizer; +use ApiPlatform\Hydra\Serializer\EntrypointNormalizer as HydraEntrypointNormalizer; +use ApiPlatform\Hydra\Serializer\HydraPrefixNameConverter; +use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer as HydraPartialCollectionViewNormalizer; +use ApiPlatform\Hydra\State\HydraLinkProcessor; +use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory; +use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer; +use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer; +use ApiPlatform\JsonApi\Serializer\ErrorNormalizer as JsonApiErrorNormalizer; +use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer; +use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer; +use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; +use ApiPlatform\JsonLd\Action\ContextAction; +use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; +use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder; +use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer; +use ApiPlatform\JsonLd\Serializer\ObjectNormalizer as JsonLdObjectNormalizer; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Laravel\ApiResource\Error; +use ApiPlatform\Laravel\Controller\ApiPlatformController; +use ApiPlatform\Laravel\Controller\DocumentationController; +use ApiPlatform\Laravel\Controller\EntrypointController; +use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension; +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; +use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; +use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface; +use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; +use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; +use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\IdentifiersExtractor as EloquentIdentifiersExtractor; +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver; +use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor; +use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder; +use ApiPlatform\Laravel\Eloquent\Serializer\SnakeCaseToCamelCaseNameConverter; +use ApiPlatform\Laravel\Eloquent\State\CollectionProvider; +use ApiPlatform\Laravel\Eloquent\State\ItemProvider; +use ApiPlatform\Laravel\Eloquent\State\LinksHandler; +use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface; +use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; +use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; +use ApiPlatform\Laravel\Exception\ErrorHandler; +use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController; +use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController; +use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory; +use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory; +use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory; +use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory; +use ApiPlatform\Laravel\Routing\IriConverter; +use ApiPlatform\Laravel\Routing\Router as UrlGeneratorRouter; +use ApiPlatform\Laravel\Routing\SkolemIriConverter; +use ApiPlatform\Laravel\Security\ResourceAccessChecker; +use ApiPlatform\Laravel\State\AccessCheckerProvider; +use ApiPlatform\Laravel\State\ParameterValidatorProvider; +use ApiPlatform\Laravel\State\SwaggerUiProcessor; +use ApiPlatform\Laravel\State\SwaggerUiProvider; +use ApiPlatform\Laravel\State\ValidateProvider; +use ApiPlatform\Metadata\Exception\NotExposedHttpException; +use ApiPlatform\Metadata\IdentifiersExtractor; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\InflectorInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; +use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator; +use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\ClassLevelAttributePropertyNameCollectionFactory; +use ApiPlatform\Metadata\Property\Factory\ConcernsPropertyNameCollectionMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyNameCollectionFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\SerializerPropertyMetadataFactory; +use ApiPlatform\Metadata\Resource\Factory\AlternateUriResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceNameCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceNameCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\FiltersResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\FormatsResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\InputOutputResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\LinkFactory; +use ApiPlatform\Metadata\Resource\Factory\LinkFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\LinkResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\NotExposedOperationResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\OperationNameResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ParameterResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\PhpDocResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\Metadata\ResourceClassResolver; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\Inflector; +use ApiPlatform\OpenApi\Factory\OpenApiFactory; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\Options; +use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; +use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use ApiPlatform\Serializer\ItemNormalizer; +use ApiPlatform\Serializer\JsonEncoder; +use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory; +use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; +use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider; +use ApiPlatform\Serializer\SerializerContextBuilder; +use ApiPlatform\State\CallableProcessor; +use ApiPlatform\State\CallableProvider; +use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\Pagination\PaginationOptions; +use ApiPlatform\State\ParameterProviderInterface; +use ApiPlatform\State\Processor\AddLinkHeaderProcessor; +use ApiPlatform\State\Processor\RespondProcessor; +use ApiPlatform\State\Processor\SerializeProcessor; +use ApiPlatform\State\Processor\WriteProcessor; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\Provider\ContentNegotiationProvider; +use ApiPlatform\State\Provider\DeserializeProvider; +use ApiPlatform\State\Provider\ParameterProvider; +use ApiPlatform\State\Provider\ReadProvider; +use ApiPlatform\State\Provider\SecurityParameterProvider; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Foundation\CachesRoutes; +use Illuminate\Http\Request; +use Illuminate\Routing\Route; +use Illuminate\Routing\RouteCollection; +use Illuminate\Routing\Router; +use Illuminate\Support\ServiceProvider; +use Negotiation\Negotiator; +use phpDocumentor\Reflection\DocBlockFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\WebLink\HttpHeaderSerializer; + +class ApiPlatformProvider extends ServiceProvider +{ + /** + * Register services. + */ + public function register(): void + { + $this->mergeConfigFrom(__DIR__.'/config/api-platform.php', 'api-platform'); + + $this->app->singleton(PropertyInfoExtractorInterface::class, function () { + $phpDocExtractor = class_exists(DocBlockFactory::class) ? new PhpDocExtractor() : null; + $reflectionExtractor = new ReflectionExtractor(); + + return new PropertyInfoExtractor( + [$reflectionExtractor], + $phpDocExtractor ? [$phpDocExtractor, $reflectionExtractor] : [$reflectionExtractor], + $phpDocExtractor ? [$phpDocExtractor] : [], + [$reflectionExtractor], + [$reflectionExtractor] + ); + }); + + $this->app->bind(LoaderInterface::class, AttributeLoader::class); + $this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class); + $this->app->singleton(ClassMetadataFactory::class, function (Application $app) { + return new ClassMetadataFactory( + new LoaderChain([ + new PropertyMetadataLoader( + $app->make(PropertyNameCollectionFactoryInterface::class), + ), + new AttributeLoader(), + ]) + ); + }); + + $this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) { + return new SerializerClassMetadataFactory($app->make(ClassMetadataFactoryInterface::class)); + }); + + $this->app->bind(PathSegmentNameGeneratorInterface::class, UnderscorePathSegmentNameGenerator::class); + + $this->app->singleton(ResourceNameCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $paths = $config->get('api-platform.resources') ?? []; + $refl = new \ReflectionClass(Error::class); + $paths[] = \dirname($refl->getFileName()); + + $logger = $app->make(LoggerInterface::class); + + foreach ($paths as $i => $path) { + if (!file_exists($path)) { + $logger->warning(\sprintf('We skipped reading resources in "%s" as the path does not exist. Please check the configuration at "api-platform.resources".', $path)); + unset($paths[$i]); + } + } + + return new ConcernsResourceNameCollectionFactory($paths, new AttributesResourceNameCollectionFactory($paths)); + }); + + $this->app->bind(ResourceClassResolverInterface::class, ResourceClassResolver::class); + $this->app->singleton(ResourceClassResolver::class, function (Application $app) { + return new EloquentResourceClassResolver(new ResourceClassResolver($app->make(ResourceNameCollectionFactoryInterface::class))); + }); + + $this->app->singleton(PropertyMetadataFactoryInterface::class, function (Application $app) { + return new PropertyInfoPropertyMetadataFactory( + $app->make(PropertyInfoExtractorInterface::class), + new EloquentPropertyMetadataFactory( + $app->make(ModelMetadata::class), + ) + ); + }); + + $this->app->extend(PropertyMetadataFactoryInterface::class, function (PropertyInfoPropertyMetadataFactory $inner, Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + + return new CachePropertyMetadataFactory( + new SchemaPropertyMetadataFactory( + $app->make(ResourceClassResolverInterface::class), + new SerializerPropertyMetadataFactory( + $app->make(SerializerClassMetadataFactory::class), + new AttributePropertyMetadataFactory( + new EloquentAttributePropertyMetadataFactory( + $inner, + ) + ), + $app->make(ResourceClassResolverInterface::class) + ), + ), + true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file') + ); + }); + + $this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + + return new CachePropertyNameCollectionMetadataFactory( + new ClassLevelAttributePropertyNameCollectionFactory( + new ConcernsPropertyNameCollectionMetadataFactory( + new EloquentPropertyNameCollectionMetadataFactory( + $app->make(ModelMetadata::class), + new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)), + $app->make(ResourceClassResolverInterface::class) + ) + ) + ), + true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file') + ); + }); + + $this->app->singleton(LinkFactoryInterface::class, function (Application $app) { + return new LinkFactory( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + ); + }); + + // TODO: add cached metadata factories + $this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + $formats = $config->get('api-platform.formats'); + + if ($config->get('api-platform.swagger_ui.enabled', false) && !isset($formats['html'])) { + $formats['html'] = ['text/html']; + } + + return new CacheResourceCollectionMetadataFactory( + new EloquentResourceCollectionMetadataFactory( + new ParameterValidationResourceMetadataCollectionFactory( + new ParameterResourceMetadataCollectionFactory( + $this->app->make(PropertyNameCollectionFactoryInterface::class), + $this->app->make(PropertyMetadataFactoryInterface::class), + new AlternateUriResourceMetadataCollectionFactory( + new FiltersResourceMetadataCollectionFactory( + new FormatsResourceMetadataCollectionFactory( + new InputOutputResourceMetadataCollectionFactory( + new PhpDocResourceMetadataCollectionFactory( + new OperationNameResourceMetadataCollectionFactory( + new LinkResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + new UriTemplateResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + $app->make(PathSegmentNameGeneratorInterface::class), + new NotExposedOperationResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + new AttributesResourceMetadataCollectionFactory( + new ConcernsResourceMetadataCollectionFactory( + null, + $app->make(LoggerInterface::class), + $config->get('api-platform.defaults', []), + $config->get('api-platform.graphql.enabled'), + ), + $app->make(LoggerInterface::class), + $config->get('api-platform.defaults', []), + $config->get('api-platform.graphql.enabled'), + ), + ) + ), + $config->get('api-platform.graphql.enabled') + ) + ) + ) + ), + $formats, + $config->get('api-platform.patch_formats'), + ) + ) + ), + $app->make('filters'), + $app->make(CamelCaseToSnakeCaseNameConverter::class) + ), + $app->make('filters') + ) + ), + true === $config->get('app.debug') ? 'array' : $config->get('cache.default', 'file') + ); + }); + + $this->app->bind(PropertyAccessorInterface::class, function () { + return new EloquentPropertyAccessor(); + }); + + $this->app->bind(NameConverterInterface::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $app->make(SnakeCaseToCamelCaseNameConverter::class)), $defaultContext); + }); + + $this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class); + + $this->app->tag([EqualsFilter::class, PartialSearchFilter::class, DateFilter::class, OrderFilter::class, RangeFilter::class], EloquentFilterInterface::class); + + $this->app->bind(FilterQueryExtension::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class)); + + return new FilterQueryExtension(new ServiceLocator($tagged)); + }); + + $this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class); + + $this->app->singleton(ItemProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class)); + + return new ItemProvider(new LinksHandler($app), new ServiceLocator($tagged)); + }); + $this->app->singleton(CollectionProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class)); + + return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged)); + }); + $this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class); + + $this->app->singleton(CallableProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(ProviderInterface::class)); + + return new CallableProvider(new ServiceLocator($tagged)); + }); + + $this->app->singleton(ReadProvider::class, function (Application $app) { + return new ReadProvider($app->make(CallableProvider::class)); + }); + + $this->app->singleton(SwaggerUiProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SwaggerUiProvider($app->make(ReadProvider::class), $app->make(OpenApiFactoryInterface::class), $config->get('api-platform.swagger_ui.enabled', false)); + }); + + $this->app->singleton(ValidateProvider::class, function (Application $app) { + return new ValidateProvider($app->make(SwaggerUiProvider::class), $app); + }); + + $this->app->singleton(DeserializeProvider::class, function (Application $app) { + return new DeserializeProvider($app->make(ValidateProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class)); + }); + + $this->app->tag([PropertyFilter::class], SerializerFilterInterface::class); + + $this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(SerializerFilterInterface::class)); + + return new SerializerFilterParameterProvider(new ServiceLocator($tagged)); + }); + $this->app->alias(SerializerFilterParameterProvider::class, 'api_platform.serializer.filter_parameter_provider'); + + $this->app->tag([SerializerFilterParameterProvider::class], ParameterProviderInterface::class); + + $this->app->singleton('filters', function (Application $app) { + return new ServiceLocator(array_merge( + iterator_to_array($app->tagged(SerializerFilterInterface::class)), + iterator_to_array($app->tagged(EloquentFilterInterface::class)) + )); + }); + + $this->app->singleton(ParameterProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class)); + $tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class); + + return new ParameterProvider( + new ParameterValidatorProvider( + new SecurityParameterProvider( + $app->make(DeserializeProvider::class), + $app->make(ResourceAccessCheckerInterface::class) + ), + ), + new ServiceLocator($tagged) + ); + }); + + $this->app->singleton(AccessCheckerProvider::class, function (Application $app) { + return new AccessCheckerProvider($app->make(ParameterProvider::class), $app->make(ResourceAccessCheckerInterface::class)); + }); + + $this->app->singleton(Negotiator::class, function (Application $app) { + return new Negotiator(); + }); + $this->app->singleton(ContentNegotiationProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ContentNegotiationProvider($app->make(AccessCheckerProvider::class), $app->make(Negotiator::class), $config->get('api-platform.formats'), $config->get('api-platform.error_formats')); + }); + + $this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class); + + $this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class); + $this->app->singleton(CallableProcessor::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $tagged = iterator_to_array($app->tagged(ProcessorInterface::class)); + + if ($config->get('api-platform.swagger_ui.enabled', false)) { + // TODO: tag SwaggerUiProcessor instead? + $tagged['api_platform.swagger_ui.processor'] = $app->make(SwaggerUiProcessor::class); + } + + return new CallableProcessor(new ServiceLocator($tagged)); + }); + + $this->app->singleton(RespondProcessor::class, function () { + return new AddLinkHeaderProcessor(new RespondProcessor(), new HttpHeaderSerializer()); + }); + + $this->app->singleton(SerializeProcessor::class, function (Application $app) { + return new SerializeProcessor($app->make(RespondProcessor::class), $app->make(Serializer::class), $app->make(SerializerContextBuilderInterface::class)); + }); + + $this->app->singleton(WriteProcessor::class, function (Application $app) { + return new WriteProcessor($app->make(SerializeProcessor::class), $app->make(CallableProcessor::class)); + }); + + $this->app->singleton(SerializerContextBuilder::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SerializerContextBuilder($app->make(ResourceMetadataCollectionFactoryInterface::class), $config->get('app.debug')); + }); + $this->app->bind(SerializerContextBuilderInterface::class, EloquentSerializerContextBuilder::class); + $this->app->singleton(EloquentSerializerContextBuilder::class, function (Application $app) { + return new EloquentSerializerContextBuilder( + $app->make(SerializerContextBuilder::class), + $app->make(PropertyNameCollectionFactoryInterface::class) + ); + }); + + $this->app->singleton(HydraLinkProcessor::class, function (Application $app) { + return new HydraLinkProcessor($app->make(WriteProcessor::class), $app->make(UrlGeneratorInterface::class)); + }); + + $this->app->bind(ProcessorInterface::class, function (Application $app) { + $config = $app['config']; + if ($config->has('api-platform.formats.jsonld')) { + return $app->make(HydraLinkProcessor::class); + } + + return $app->make(WriteProcessor::class); + }); + + $this->app->singleton(ObjectNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new ObjectNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(DateTimeNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new DateTimeNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(DateTimeZoneNormalizer::class, function () { + return new DateTimeZoneNormalizer(); + }); + + $this->app->singleton(DateIntervalNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new DateIntervalNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(JsonEncoder::class, function () { + return new JsonEncoder('jsonld'); + }); + + $this->app->bind(IriConverterInterface::class, IriConverter::class); + $this->app->singleton(IriConverter::class, function (Application $app) { + return new IriConverter($app->make(CallableProvider::class), $app->make(OperationMetadataFactoryInterface::class), $app->make(UrlGeneratorRouter::class), $app->make(IdentifiersExtractorInterface::class), $app->make(ResourceClassResolverInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(SkolemIriConverter::class)); + }); + + $this->app->singleton(SkolemIriConverter::class, function (Application $app) { + return new SkolemIriConverter($app->make(UrlGeneratorRouter::class)); + }); + + $this->app->bind(IdentifiersExtractorInterface::class, IdentifiersExtractor::class); + $this->app->singleton(IdentifiersExtractor::class, function (Application $app) { + return new EloquentIdentifiersExtractor( + new IdentifiersExtractor( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(PropertyAccessorInterface::class) + ) + ); + }); + + $this->app->bind(UrlGeneratorInterface::class, UrlGeneratorRouter::class); + $this->app->singleton(UrlGeneratorRouter::class, function (Application $app) { + $request = $app->make('request'); + // https://github.com/laravel/framework/blob/2bfb70bca53e24227a6f921f39d84ba452efd8e0/src/Illuminate/Routing/CompiledRouteCollection.php#L112 + $trimmedRequest = $request->duplicate(); + $parts = explode('?', $request->server->get('REQUEST_URI'), 2); + $trimmedRequest->server->set( + 'REQUEST_URI', + rtrim($parts[0], '/').(isset($parts[1]) ? '?'.$parts[1] : '') + ); + + $urlGenerator = new UrlGeneratorRouter($app->make(Router::class)); + $urlGenerator->setContext((new RequestContext())->fromRequest($trimmedRequest)); + + return $urlGenerator; + }); + + $this->app->bind(ContextBuilderInterface::class, JsonLdContextBuilder::class); + $this->app->singleton(JsonLdContextBuilder::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonLdContextBuilder( + $app->make(ResourceNameCollectionFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(UrlGeneratorInterface::class), + $app->make(IriConverterInterface::class), + $app->make(NameConverterInterface::class), + $defaultContext + ); + }); + + $this->app->singleton(HydraEntrypointNormalizer::class, function (Application $app) { + return new HydraEntrypointNormalizer($app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(IriConverterInterface::class), $app->make(UrlGeneratorInterface::class)); + }); + + $this->app->singleton(ResourceAccessCheckerInterface::class, function () { + return new ResourceAccessChecker(); + }); + + $this->app->singleton(ItemNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new ItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $app->make(LoggerInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + $defaultContext + ); + }); + + $this->app->bind(AnonymousContextBuilderInterface::class, JsonLdContextBuilder::class); + + $this->app->singleton(JsonLdObjectNormalizer::class, function (Application $app) { + return new JsonLdObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(AnonymousContextBuilderInterface::class) + ); + }); + + $this->app->singleton(HalCollectionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new HalCollectionNormalizer( + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.pagination.page_parameter_name'), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(HalObjectNormalizer::class, function (Application $app) { + return new HalObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class) + ); + }); + + $this->app->singleton(HalItemNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HalItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + ); + }); + + $this->app->singleton(Options::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Options( + title: $config->get('api-platform.title', ''), + description: $config->get('api-platform.description', ''), + version: $config->get('api-platform.version', ''), + oAuthEnabled: $config->get('api-platform.swagger_ui.oauth.enabled', false), + oAuthType: $config->get('api-platform.swagger_ui.oauth.type', null), + oAuthFlow: $config->get('api-platform.swagger_ui.oauth.flow', null), + oAuthTokenUrl: $config->get('api-platform.swagger_ui.oauth.tokenUrl', null), + oAuthAuthorizationUrl: $config->get('api-platform.swagger_ui.oauth.authorizationUrl', null), + oAuthRefreshUrl: $config->get('api-platform.swagger_ui.oauth.refreshUrl', null), + oAuthScopes: $config->get('api-platform.swagger_ui.oauth.scopes', []), + apiKeys: $config->get('api-platform.swagger_ui.apiKeys', []), + ); + }); + + $this->app->singleton(SwaggerUiProcessor::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SwaggerUiProcessor( + urlGenerator: $app->make(UrlGeneratorInterface::class), + normalizer: $app->make(NormalizerInterface::class), + openApiOptions: $app->make(Options::class), + oauthClientId: $config->get('api-platform.swagger_ui.oauth.clientId'), + oauthClientSecret: $config->get('api-platform.swagger_ui.oauth.clientSecret'), + oauthPkce: $config->get('api-platform.swagger_ui.oauth.pkce', false), + ); + }); + + $this->app->singleton(DocumentationController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new DocumentationController($app->make(ResourceNameCollectionFactoryInterface::class), $config->get('api-platform.title') ?? '', $config->get('api-platform.description') ?? '', $config->get('api-platform.version') ?? '', $app->make(OpenApiFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $app->make(Negotiator::class), $config->get('api-platform.docs_formats'), $config->get('api-platform.swagger_ui.enabled', false)); + }); + + $this->app->singleton(EntrypointController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new EntrypointController($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $config->get('api-platform.docs_formats')); + }); + + $this->app->singleton(Pagination::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Pagination($config->get('api-platform.pagination'), []); + }); + + $this->app->singleton(PaginationOptions::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaults = $config->get('api-platform.defaults'); + $pagination = $config->get('api-platform.pagination'); + + return new PaginationOptions( + $defaults['pagination_enabled'], + $pagination['page_parameter_name'], + $defaults['pagination_client_items_per_page'], + $pagination['items_per_page_parameter_name'], + $defaults['pagination_client_enabled'], + $pagination['enabled_parameter_name'], + $defaults['pagination_items_per_page'], + $defaults['pagination_maximum_items_per_page'], + $defaults['pagination_partial'], + $defaults['pagination_client_partial'], + $pagination['partial_parameter_name'], + ); + }); + + $this->app->bind(OpenApiFactoryInterface::class, OpenApiFactory::class); + $this->app->singleton(OpenApiFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new OpenApiFactory( + $app->make(ResourceNameCollectionFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(SchemaFactoryInterface::class), + null, + $config->get('api-platform.formats'), + $app->make(Options::class), + $app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null, + // ?RouterInterface $router = null + ); + }); + + $this->app->bind(DefinitionNameFactoryInterface::class, DefinitionNameFactory::class); + $this->app->singleton(DefinitionNameFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new DefinitionNameFactory($config->get('api-platform.formats')); + }); + + $this->app->singleton(SchemaFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SchemaFactory( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.formats'), + $app->make(DefinitionNameFactoryInterface::class), + ); + }); + $this->app->singleton(JsonApiSchemaFactory::class, function (Application $app) { + return new JsonApiSchemaFactory( + $app->make(SchemaFactory::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(DefinitionNameFactoryInterface::class), + ); + }); + $this->app->singleton(HydraSchemaFactory::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraSchemaFactory( + $app->make(JsonApiSchemaFactory::class), + $defaultContext + ); + }); + + $this->app->bind(SchemaFactoryInterface::class, HydraSchemaFactory::class); + + $this->app->singleton(OpenApiNormalizer::class, function (Application $app) { + return new OpenApiNormalizer($app->make(ObjectNormalizer::class)); + }); + + $this->app->singleton(HydraDocumentationNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraDocumentationNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(UrlGeneratorInterface::class), + $app->make(NameConverterInterface::class), + $defaultContext + ); + }); + + $this->app->singleton(HydraPartialCollectionViewNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraPartialCollectionViewNormalizer( + new HydraCollectionFiltersNormalizer( + new HydraCollectionNormalizer( + $app->make(ContextBuilderInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $defaultContext + ), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + null, // filterLocator, we use only Parameters with Laravel and we don't need to call filters there + $defaultContext + ), + 'page', + 'pagination', + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyAccessorInterface::class), + $config->get('api-platform.url_generation_strategy', UrlGeneratorInterface::ABS_PATH), + $defaultContext, + ); + }); + + $this->app->singleton(ReservedAttributeNameConverter::class, function (Application $app) { + return new ReservedAttributeNameConverter($app->make(NameConverterInterface::class)); + }); + + if (interface_exists(FieldsBuilderEnumInterface::class)) { + $this->registerGraphQl($this->app); + } + + $this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) { + return new JsonApiEntrypointNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(UrlGeneratorInterface::class), + ); + }); + + $this->app->singleton(JsonApiCollectionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new JsonApiCollectionNormalizer( + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.pagination.page_parameter_name'), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(JsonApiItemNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonApiItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + null + // $app->make(TagCollectorInterface::class), + ); + }); + + $this->app->singleton(JsonApiErrorNormalizer::class, function (Application $app) { + return new JsonApiErrorNormalizer( + $app->make(JsonApiItemNormalizer::class), + ); + }); + + $this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) { + return new JsonApiObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->bind(SerializerInterface::class, Serializer::class); + $this->app->bind(NormalizerInterface::class, Serializer::class); + $this->app->singleton(Serializer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $list = new \SplPriorityQueue(); + $list->insert($app->make(HydraEntrypointNormalizer::class), -800); + $list->insert($app->make(HydraPartialCollectionViewNormalizer::class), -800); + $list->insert($app->make(HalCollectionNormalizer::class), -800); + $list->insert($app->make(HalEntrypointNormalizer::class), -985); + $list->insert($app->make(HalObjectNormalizer::class), -995); + $list->insert($app->make(HalItemNormalizer::class), -890); + $list->insert($app->make(JsonLdItemNormalizer::class), -890); + $list->insert($app->make(JsonLdObjectNormalizer::class), -995); + $list->insert($app->make(ArrayDenormalizer::class), -990); + $list->insert($app->make(DateTimeZoneNormalizer::class), -915); + $list->insert($app->make(DateIntervalNormalizer::class), -915); + $list->insert($app->make(DateTimeNormalizer::class), -910); + $list->insert($app->make(ObjectNormalizer::class), -1000); + $list->insert($app->make(ItemNormalizer::class), -895); + $list->insert($app->make(OpenApiNormalizer::class), -780); + $list->insert($app->make(HydraDocumentationNormalizer::class), -790); + + $list->insert($app->make(JsonApiEntrypointNormalizer::class), -800); + $list->insert($app->make(JsonApiCollectionNormalizer::class), -985); + $list->insert($app->make(JsonApiItemNormalizer::class), -890); + $list->insert($app->make(JsonApiErrorNormalizer::class), -790); + $list->insert($app->make(JsonApiObjectNormalizer::class), -995); + + if (interface_exists(FieldsBuilderEnumInterface::class)) { + $list->insert($app->make(GraphQlItemNormalizer::class), -890); + $list->insert($app->make(GraphQlObjectNormalizer::class), -995); + $list->insert($app->make(GraphQlErrorNormalizer::class), -790); + $list->insert($app->make(GraphQlValidationExceptionNormalizer::class), -780); + $list->insert($app->make(GraphQlHttpExceptionNormalizer::class), -780); + $list->insert($app->make(GraphQlRuntimeExceptionNormalizer::class), -780); + } + + // TODO: unused + implement hal/jsonapi ? + // $list->insert($dataUriNormalizer, -920); + // $list->insert($unwrappingDenormalizer, 1000); + // $list->insert($jsonserializableNormalizer, -900); + // $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ? + + return new Serializer( + iterator_to_array($list), + [ + new JsonEncoder('json'), + $app->make(JsonEncoder::class), + new JsonEncoder('jsonopenapi'), + new JsonEncoder('jsonapi'), + new JsonEncoder('jsonhal'), + new CsvEncoder(), + ] + ); + }); + + $this->app->singleton(JsonLdItemNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonLdItemNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ContextBuilderInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceAccessCheckerInterface::class) + ); + }); + + $this->app->singleton( + ExceptionHandlerInterface::class, + function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ErrorHandler( + $app, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ApiPlatformController::class), + $app->make(IdentifiersExtractorInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(Negotiator::class), + $config->get('api-platform.exception_to_status'), + $config->get('app.debug') + ); + } + ); + + $this->app->singleton(InflectorInterface::class, function (Application $app) { + return new Inflector(); + }); + + if ($this->app->runningInConsole()) { + $this->commands([ + Console\InstallCommand::class, + Console\Maker\MakeStateProcessorCommand::class, + Console\Maker\MakeStateProviderCommand::class, + ]); + } + } + + private function registerGraphQl(Application $app): void + { + $this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) { + return new GraphQlItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(IdentifiersExtractorInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(SerializerClassMetadataFactory::class), + null, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class) + ); + }); + + $this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) { + return new GraphQlObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(IdentifiersExtractorInterface::class), + ); + }); + + $this->app->singleton(GraphQlErrorNormalizer::class, function () { + return new GraphQlErrorNormalizer(); + }); + + $this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status')); + }); + + $this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () { + return new GraphQlHttpExceptionNormalizer(); + }); + + $this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () { + return new GraphQlHttpExceptionNormalizer(); + }); + + $app->singleton('api_platform.graphql.type_locator', function (Application $app) { + $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); + + return new ServiceLocator($tagged); + }); + + $app->singleton(TypesFactoryInterface::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); + + return new TypesFactory($app->make('api_platform.graphql.type_locator'), array_keys($tagged)); + }); + $app->singleton(TypesContainerInterface::class, function () { + return new TypesContainer(); + }); + + $app->singleton(ResourceFieldResolver::class, function (Application $app) { + return new ResourceFieldResolver($app->make(IriConverterInterface::class)); + }); + + $app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) { + return new TypeBuilder( + $app->make(TypesContainerInterface::class), + $app->make(ResourceFieldResolver::class), + null, + $app->make(Pagination::class) + ); + }); + + $app->singleton(TypeConverterInterface::class, function (Application $app) { + return new TypeConverter( + $app->make(ContextAwareTypeBuilderInterface::class), + $app->make(TypesContainerInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + ); + }); + + $app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) { + return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class)); + }); + + $app->singleton(GraphQlReadProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlReadProvider( + $this->app->make(CallableProvider::class), + $app->make(IriConverterInterface::class), + $app->make(GraphQlSerializerContextBuilder::class), + $config->get('api-platform.graphql.nesting_separator') ?? '__' + ); + }); + $app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read'); + + $app->singleton(ResolverProvider::class, function (Application $app) { + $resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver')); + + return new ResolverProvider( + $app->make(GraphQlReadProvider::class), + new ServiceLocator($resolvers), + ); + }); + + $app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver'); + + $app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) { + return new GraphQlDenormalizeProvider( + $this->app->make(ResolverProvider::class), + $app->make(SerializerInterface::class), + $app->make(GraphQlSerializerContextBuilder::class) + ); + }); + + $app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize'); + + $app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) { + $tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class)); + $tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class); + + return new ParameterProvider( + new ParameterValidatorProvider( + new SecurityParameterProvider( + $app->make(GraphQlDenormalizeProvider::class), + $app->make(ResourceAccessCheckerInterface::class) + ), + ), + new ServiceLocator($tagged) + ); + }); + + $app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) { + return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class)); + }); + + $app->singleton(NormalizeProcessor::class, function (Application $app) { + return new NormalizeProcessor( + $app->make(SerializerInterface::class), + $app->make(GraphQlSerializerContextBuilder::class), + $app->make(Pagination::class) + ); + }); + $app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize'); + + $app->singleton('api_platform.graphql.state_processor', function (Application $app) { + return new WriteProcessor( + $app->make('api_platform.graphql.state_processor.normalize'), + $app->make(CallableProcessor::class), + ); + }); + + $app->singleton(ResolverFactoryInterface::class, function (Application $app) { + return new ResolverFactory( + $app->make('api_platform.graphql.state_provider.access_checker'), + $app->make('api_platform.graphql.state_processor') + ); + }); + + $app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new FieldsBuilder( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(TypesContainerInterface::class), + $app->make(ContextAwareTypeBuilderInterface::class), + $app->make(TypeConverterInterface::class), + $app->make(ResolverFactoryInterface::class), + $app->make('filters'), + $app->make(Pagination::class), + $app->make(NameConverterInterface::class), + $config->get('api-platform.graphql.nesting_separator') ?? '__', + $app->make(InflectorInterface::class) + ); + }); + + $app->singleton(SchemaBuilderInterface::class, function (Application $app) { + return new SchemaBuilder($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(TypesFactoryInterface::class), $app->make(TypesContainerInterface::class), $app->make(FieldsBuilderEnumInterface::class)); + }); + + $app->singleton(ErrorHandlerInterface::class, function () { + return new GraphQlErrorHandler(); + }); + + $app->singleton(ExecutorInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false); + }); + + $app->singleton(GraphiQlController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $prefix = $config->get('api-platform.defaults.route_prefix') ?? ''; + + return new GraphiQlController($prefix); + }); + + $app->singleton(GraphQlEntrypointController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlEntrypointController( + $app->make(SchemaBuilderInterface::class), + $app->make(ExecutorInterface::class), + $app->make(GraphiQlController::class), + $app->make(SerializerInterface::class), + $app->make(ErrorHandlerInterface::class), + debug: $config->get('app.debug'), + negotiator: $app->make(Negotiator::class), + formats: $config->get('api-platform.formats') + ); + }); + } + + /** + * Bootstrap services. + */ + public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, Router $router): void + { + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__.'/config/api-platform.php' => $this->app->configPath('api-platform.php'), + ], 'api-platform-config'); + + $this->publishes([ + __DIR__.'/public' => $this->app->publicPath('vendor/api-platform'), + ], ['api-platform-assets', 'public']); + } + + $this->loadViewsFrom(__DIR__.'/resources/views', 'api-platform'); + + $config = $this->app['config']; + + if ($config->get('api-platform.graphql.enabled')) { + $fieldsBuilder = $this->app->make(FieldsBuilderEnumInterface::class); + $typeBuilder = $this->app->make(ContextAwareTypeBuilderInterface::class); + $typeBuilder->setFieldsBuilderLocator(new ServiceLocator(['api_platform.graphql.fields_builder' => $fieldsBuilder])); + } + + if (!$this->shouldRegisterRoutes()) { + return; + } + + $globalMiddlewares = $config->get('api-platform.routes.middleware'); + $routeCollection = new RouteCollection(); + foreach ($resourceNameCollectionFactory->create() as $resourceClass) { + foreach ($resourceMetadataFactory->create($resourceClass) as $resourceMetadata) { + foreach ($resourceMetadata->getOperations() as $operation) { + $uriTemplate = $operation->getUriTemplate(); + // _format is read by the middleware + $uriTemplate = $operation->getRoutePrefix().str_replace('{._format}', '{_format?}', $uriTemplate); + $route = (new Route([$operation->getMethod()], $uriTemplate, [ApiPlatformController::class, '__invoke'])) + ->name($operation->getName()) + ->setDefaults(['_api_operation_name' => $operation->getName(), '_api_resource_class' => $operation->getClass()]); + + $route->middleware(ApiPlatformMiddleware::class.':'.$operation->getName()); + $route->middleware($globalMiddlewares); + $route->middleware($operation->getMiddleware()); + + $routeCollection->add($route); + } + } + } + + $prefix = $config->get('api-platform.defaults.route_prefix') ?? ''; + $route = new Route(['GET'], $prefix.'/contexts/{shortName?}{_format?}', [ContextAction::class, '__invoke']); + $route->name('api_jsonld_context'); + $route->middleware(ApiPlatformMiddleware::class); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + $route = new Route(['GET'], $prefix.'/docs{_format?}', function (Request $request, Application $app) { + $documentationAction = $app->make(DocumentationController::class); + + return $documentationAction->__invoke($request); + }); + $route->name('api_doc'); + $route->middleware(ApiPlatformMiddleware::class); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + + $route = new Route(['GET'], $prefix.'/.well-known/genid/{id}', function (): void { + throw new NotExposedHttpException('This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.'); + }); + $route->name('api_genid'); + $route->middleware(ApiPlatformMiddleware::class); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + + if ($config->get('api-platform.graphql.enabled')) { + $route = new Route(['POST', 'GET'], $prefix.'/graphql', function (Application $app, Request $request) { + $entrypointAction = $app->make(GraphQlEntrypointController::class); + + return $entrypointAction->__invoke($request); + }); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + + $route = new Route(['GET'], $prefix.'/graphiql', function (Application $app) { + $controller = $app->make(GraphiQlController::class); + + return $controller->__invoke(); + }); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + } + + $route = new Route(['GET'], $prefix.'/{index?}{_format?}', function (Request $request, Application $app) { + $entrypointAction = $app->make(EntrypointController::class); + + return $entrypointAction->__invoke($request); + }); + $route->where('index', 'index'); + $route->name('api_entrypoint'); + $route->middleware(ApiPlatformMiddleware::class); + $route->middleware($globalMiddlewares); + $routeCollection->add($route); + + $router->setRoutes($routeCollection); + } + + private function shouldRegisterRoutes(): bool + { + if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) { + return false; + } + + return true; + } +} diff --git a/src/Laravel/ApiResource/Error.php b/src/Laravel/ApiResource/Error.php new file mode 100644 index 00000000000..69e0bbc4e1c --- /dev/null +++ b/src/Laravel/ApiResource/Error.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\ApiResource; + +use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Error as Operation; +use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\Ignore; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\WebLink\Link; + +#[ErrorResource( + types: ['hydra:Error'], + openapi: false, + operations: [ + new Operation( + name: '_api_errors_problem', + outputFormats: ['json' => ['application/problem+json']], + normalizationContext: [ + 'groups' => ['jsonproblem'], + 'skip_null_values' => true, + ], + uriTemplate: '/errors/{status}' + ), + new Operation( + name: '_api_errors_hydra', + outputFormats: ['jsonld' => ['application/problem+json']], + normalizationContext: [ + 'groups' => ['jsonld'], + 'skip_null_values' => true, + ], + links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')], + uriTemplate: '/errors/{status}.jsonld' + ), + new Operation( + name: '_api_errors_jsonapi', + outputFormats: ['jsonapi' => ['application/vnd.api+json']], + normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true], + uriTemplate: '/errors/{status}.jsonapi' + ), + ], + graphQlOperations: [] +)] +class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface +{ + /** + * @var array + */ + private array $originalTrace; + + /** + * @param array $headers + * @param array $originalTrace + */ + public function __construct( + private readonly string $title, + private readonly string $detail, + #[ApiProperty(identifier: true)] private int $status, + array $originalTrace, + private readonly ?string $instance = null, + private string $type = 'about:blank', + private array $headers = [], + ) { + parent::__construct(); + + $this->originalTrace = []; + foreach ($originalTrace as $i => $t) { + unset($t['args']); // we don't want arguments in our JSON traces, especially with xdebug + $this->originalTrace[$i] = $t; + } + } + + /** + * @return array + */ + #[SerializedName('trace')] + #[Groups(['trace'])] + public function getOriginalTrace(): array + { + return $this->originalTrace; + } + + #[SerializedName('description')] + public function getDescription(): string + { + return $this->detail; + } + + public static function createFromException(\Exception|\Throwable $exception, int $status): self + { + $headers = ($exception instanceof SymfonyHttpExceptionInterface || $exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : []; + + return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers); + } + + /** + * @return array + */ + #[Ignore] + public function getHeaders(): array + { + return $this->headers; + } + + #[Ignore] + public function getStatusCode(): int + { + return $this->status; + } + + #[Groups(['jsonapi'])] + public function getId(): string + { + return (string) $this->status; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): void + { + $this->headers = $headers; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getType(): string + { + return $this->type; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getTitle(): ?string + { + return $this->title; + } + + public function setType(string $type): void + { + $this->type = $type; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getStatus(): ?int + { + return $this->status; + } + + public function setStatus(int $status): void + { + $this->status = $status; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getDetail(): ?string + { + return $this->detail; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getInstance(): ?string + { + return $this->instance; + } +} diff --git a/src/Laravel/ApiResource/ValidationError.php b/src/Laravel/ApiResource/ValidationError.php new file mode 100644 index 00000000000..92197a74e2b --- /dev/null +++ b/src/Laravel/ApiResource/ValidationError.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\ApiResource; + +use ApiPlatform\Metadata\Error as ErrorOperation; +use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\WebLink\Link; + +/** + * Thrown when a validation error occurs. + * + * @author Kévin Dunglas + */ +#[ErrorResource( + uriTemplate: '/validation_errors/{id}', + status: 422, + openapi: false, + uriVariables: ['id'], + shortName: 'ValidationError', + operations: [ + new ErrorOperation( + name: '_api_validation_errors_problem', + outputFormats: ['json' => ['application/problem+json']], + normalizationContext: ['groups' => ['json'], + 'skip_null_values' => true, + ], + uriTemplate: '/validation_errors/{id}' + ), + new ErrorOperation( + name: '_api_validation_errors_hydra', + outputFormats: ['jsonld' => ['application/problem+json']], + links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')], + normalizationContext: [ + 'groups' => ['jsonld'], + 'skip_null_values' => true, + ], + uriTemplate: '/validation_errors/{id}.jsonld' + ), + new ErrorOperation( + name: '_api_validation_errors_jsonapi', + outputFormats: ['jsonapi' => ['application/vnd.api+json']], + normalizationContext: [ + 'groups' => ['jsonapi'], + 'skip_null_values' => true, + ], + uriTemplate: '/validation_errors/{id}.jsonapi' + ), + ], + graphQlOperations: [] +)] +class ValidationError extends RuntimeException implements \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface +{ + private int $status = 422; + private string $id; + + /** + * @param array $violations + */ + public function __construct(string $message = '', mixed $code = null, ?\Throwable $previous = null, protected array $violations = []) + { + $this->id = (string) $code; + $this->setDetail($message); + parent::__construct($message ?: $this->__toString(), 422, $previous); + } + + public function getId(): string + { + return $this->id; + } + + #[SerializedName('description')] + #[Groups(['jsonld', 'json'])] + public function getDescription(): string + { + return $this->detail; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getType(): string + { + return '/validation_errors/'.$this->id; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getTitle(): ?string + { + return 'Validation Error'; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + private string $detail; + + public function getDetail(): ?string + { + return $this->detail; + } + + public function setDetail(string $detail): void + { + $this->detail = $detail; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getStatus(): ?int + { + return $this->status; + } + + public function setStatus(int $status): void + { + $this->status = $status; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getInstance(): ?string + { + return null; + } + + /** + * @return array + */ + #[SerializedName('violations')] + #[Groups(['json', 'jsonld', 'jsonapi'])] + public function getViolations(): array + { + return $this->violations; + } + + public function getStatusCode(): int + { + return $this->status; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return []; + } +} diff --git a/src/Laravel/CONTRIBUTING.md b/src/Laravel/CONTRIBUTING.md new file mode 100644 index 00000000000..7e6574ad2e4 --- /dev/null +++ b/src/Laravel/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to the Laravel Integration of API Platform + +Pull requests should be made at https://github.com/api-plaform/core + +## Tests + + cd src/Laravel + composer global require soyuka/pmu + composer global link ../../ + vendor/bin/testbench workbench:build + vendor/bin/testbench api-platform:install + vendor/bin/testbench package:test + # or + vendor/bin/phpunit + +A command is available to remove the database: + + vendor/bin/testbench workbench:drop-sqlite-db + +## Starting the Test App + +The test server is also available through: + + vendor/bin/testbench serve diff --git a/src/Laravel/Console/InstallCommand.php b/src/Laravel/Console/InstallCommand.php new file mode 100644 index 00000000000..ebd48482702 --- /dev/null +++ b/src/Laravel/Console/InstallCommand.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console; + +use Illuminate\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; + +#[AsCommand(name: 'api-platform:install')] +class InstallCommand extends Command +{ + /** + * @var string + */ + protected $signature = 'api-platform:install'; + + /** + * @var string + */ + protected $description = 'Install all of the API Platform resources'; + + /** + * Execute the console command. + */ + public function handle(): void + { + $this->comment('Publishing API Platform Assets...'); + $this->callSilent('vendor:publish', ['--tag' => 'api-platform-assets']); + + $this->comment('Publishing API Platform Configuration...'); + $this->callSilent('vendor:publish', ['--tag' => 'api-platform-config']); + + $this->info('API Platform installed successfully.'); + } +} diff --git a/src/Laravel/Console/Maker/AbstractMakeStateCommand.php b/src/Laravel/Console/Maker/AbstractMakeStateCommand.php new file mode 100644 index 00000000000..c2359af59cf --- /dev/null +++ b/src/Laravel/Console/Maker/AbstractMakeStateCommand.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\AppServiceProviderTagger; +use ApiPlatform\Laravel\Console\Maker\Utils\StateTemplateGenerator; +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; +use ApiPlatform\Laravel\Console\Maker\Utils\SuccessMessageTrait; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +abstract class AbstractMakeStateCommand extends Command +{ + use SuccessMessageTrait; + + public function __construct( + private readonly Filesystem $filesystem, + private readonly StateTemplateGenerator $stateTemplateGenerator, + private readonly AppServiceProviderTagger $appServiceProviderTagger, + ) { + parent::__construct(); + } + + /** + * @throws FileNotFoundException + */ + public function handle(): int + { + $stateName = $this->askForStateName(); + + $directoryPath = base_path('app/State/'); + $this->filesystem->ensureDirectoryExists($directoryPath); + + $filePath = $this->stateTemplateGenerator->getFilePath($directoryPath, $stateName); + if ($this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" can\'t be generated because it already exists.', $filePath)); + + return self::FAILURE; + } + + $this->stateTemplateGenerator->generate($filePath, $stateName, $this->getStateType()); + if (!$this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" could not be created.', $filePath)); + + return self::FAILURE; + } + + $this->appServiceProviderTagger->addTagToServiceProvider($stateName, $this->getStateType()); + + $this->writeSuccessMessage($filePath, $this->getStateType()); + + return self::SUCCESS; + } + + protected function askForStateName(): string + { + do { + $stateType = $this->getStateType()->name; + $stateName = $this->ask(\sprintf('Choose a class name for your state %s (e.g. AwesomeState%s)', strtolower($stateType), ucfirst($stateType))); + if (empty($stateName)) { + $this->error('[ERROR] This value cannot be blank.'); + } + } while (empty($stateName)); + + return $stateName; + } + + abstract protected function getStateType(): StateTypeEnum; +} diff --git a/src/Laravel/Console/Maker/MakeStateProcessorCommand.php b/src/Laravel/Console/Maker/MakeStateProcessorCommand.php new file mode 100644 index 00000000000..960ae42582b --- /dev/null +++ b/src/Laravel/Console/Maker/MakeStateProcessorCommand.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; + +final class MakeStateProcessorCommand extends AbstractMakeStateCommand +{ + protected $signature = 'make:state-processor'; + protected $description = 'Creates an API Platform state processor'; + + protected function getStateType(): StateTypeEnum + { + return StateTypeEnum::Processor; + } +} diff --git a/src/Laravel/Console/Maker/MakeStateProviderCommand.php b/src/Laravel/Console/Maker/MakeStateProviderCommand.php new file mode 100644 index 00000000000..ebb89b60327 --- /dev/null +++ b/src/Laravel/Console/Maker/MakeStateProviderCommand.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; + +final class MakeStateProviderCommand extends AbstractMakeStateCommand +{ + protected $signature = 'make:state-provider'; + protected $description = 'Creates an API Platform state provider'; + + protected function getStateType(): StateTypeEnum + { + return StateTypeEnum::Provider; + } +} diff --git a/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.tpl.php b/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.tpl.php new file mode 100644 index 00000000000..3dfbe22549c --- /dev/null +++ b/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.tpl.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class AppServiceProviderTagger +{ + /** @var string */ + private const APP_SERVICE_PROVIDER_PATH = 'Providers/AppServiceProvider.php'; + + /** @var string */ + private const ITEM_PROVIDER_USE_STATEMENT = 'use ApiPlatform\State\ProviderInterface;'; + + /** @var string */ + private const ITEM_PROCESSOR_USE_STATEMENT = 'use ApiPlatform\State\ProcessorInterface;'; + + public function __construct(private Filesystem $filesystem) + { + } + + /** + * @throws FileNotFoundException + */ + public function addTagToServiceProvider(string $providerName, StateTypeEnum $stateTypeEnum): void + { + $appServiceProviderPath = app_path(self::APP_SERVICE_PROVIDER_PATH); + if (!$this->filesystem->exists($appServiceProviderPath)) { + throw new \RuntimeException('The AppServiceProvider is missing!'); + } + + $serviceProviderContent = $this->filesystem->get($appServiceProviderPath); + + $this->addUseStatement($serviceProviderContent, $this->getStateTypeStatement($stateTypeEnum)); + $this->addUseStatement($serviceProviderContent, \sprintf('use App\\State\\%s;', $providerName)); + $this->addTag($serviceProviderContent, $providerName, $appServiceProviderPath, $stateTypeEnum); + } + + private function addUseStatement(string &$content, string $useStatement): void + { + if (!str_contains($content, $useStatement)) { + $content = preg_replace( + '/^(namespace\s[^;]+;\s*)(\n)/m', + "$1\n$useStatement$2", + $content, + 1 + ); + } + } + + private function addTag(string &$content, string $stateName, string $serviceProviderPath, StateTypeEnum $stateTypeEnum): void + { + $tagStatement = \sprintf("\n\n\t\t\$this->app->tag(%s::class, %sInterface::class);", $stateName, $stateTypeEnum->name); + + if (!str_contains($content, $tagStatement)) { + $content = preg_replace( + '/(public function register\(\)[^{]*{)(.*?)(\s*}\s*})/s', + "$1$2$tagStatement$3", + $content + ); + + $this->filesystem->put($serviceProviderPath, $content); + } + } + + private function getStateTypeStatement(StateTypeEnum $stateTypeEnum): string + { + return match ($stateTypeEnum) { + StateTypeEnum::Provider => self::ITEM_PROVIDER_USE_STATEMENT, + StateTypeEnum::Processor => self::ITEM_PROCESSOR_USE_STATEMENT, + }; + } +} diff --git a/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php b/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php new file mode 100644 index 00000000000..2082d512f76 --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class StateTemplateGenerator +{ + public function __construct(private Filesystem $filesystem) + { + } + + public function getFilePath(string $directoryPath, string $stateFileName): string + { + return $directoryPath.$stateFileName.'.php'; + } + + /** + * @throws FileNotFoundException + */ + public function generate(string $pathLink, string $stateClassName, StateTypeEnum $stateTypeEnum): void + { + $namespace = 'App\\State'; + $template = $this->loadTemplate($stateTypeEnum); + + $content = strtr($template, [ + '{{ namespace }}' => $namespace, + '{{ class_name }}' => $stateClassName, + ]); + + $this->filesystem->put($pathLink, $content); + } + + /** + * @throws FileNotFoundException + */ + private function loadTemplate(StateTypeEnum $stateTypeEnum): string + { + $templateFile = match ($stateTypeEnum) { + StateTypeEnum::Provider => 'StateProvider.tpl.php', + StateTypeEnum::Processor => 'StateProcessor.tpl.php', + }; + + $templatePath = \dirname(__DIR__).'/Resources/skeleton/'.$templateFile; + + return $this->filesystem->get($templatePath); + } +} diff --git a/src/Operation/PathSegmentNameGeneratorInterface.php b/src/Laravel/Console/Maker/Utils/StateTypeEnum.php similarity index 64% rename from src/Operation/PathSegmentNameGeneratorInterface.php rename to src/Laravel/Console/Maker/Utils/StateTypeEnum.php index 4486babb640..a3c97de623c 100644 --- a/src/Operation/PathSegmentNameGeneratorInterface.php +++ b/src/Laravel/Console/Maker/Utils/StateTypeEnum.php @@ -11,8 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Operation; +namespace ApiPlatform\Laravel\Console\Maker\Utils; -interface PathSegmentNameGeneratorInterface extends \ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface +enum StateTypeEnum { + case Provider; + case Processor; } diff --git a/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php b/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php new file mode 100644 index 00000000000..e4f112d23c2 --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +trait SuccessMessageTrait +{ + private function writeSuccessMessage(string $filePath, StateTypeEnum $stateTypeEnum): void + { + $stateText = strtolower($stateTypeEnum->name); + + $this->newLine(); + $this->line(' '); + $this->line(' Success! '); + $this->line(' '); + $this->newLine(); + $this->line('created: '.$filePath.''); + $this->newLine(); + $this->line("Next: Open your new state $stateText class and start customizing it."); + } +} diff --git a/src/Laravel/Controller/ApiPlatformController.php b/src/Laravel/Controller/ApiPlatformController.php new file mode 100644 index 00000000000..7fe2ef969d7 --- /dev/null +++ b/src/Laravel/Controller/ApiPlatformController.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Foundation\Application; +use Illuminate\Http\Request; +use Illuminate\Routing\Controller; +use Symfony\Component\HttpFoundation\Response; + +class ApiPlatformController extends Controller +{ + /** + * @param ProviderInterface $provider + * @param ProcessorInterface|object|null, Response> $processor + */ + public function __construct( + protected OperationMetadataFactory $operationMetadataFactory, + protected ProviderInterface $provider, + protected ProcessorInterface $processor, + protected Application $app, + ) { + } + + /** + * Display a listing of the resource. + */ + public function __invoke(Request $request): Response + { + $operation = $request->attributes->get('_api_operation'); + if (!$operation) { + throw new \RuntimeException('Operation not found.'); + } + + if (!$operation instanceof HttpOperation) { + throw new \LogicException('Operation is not an HttpOperation.'); + } + + $uriVariables = $this->getUriVariables($request, $operation); + $request->attributes->set('_api_uri_variables', $uriVariables); + // at some point we could introduce that back + // if ($this->uriVariablesConverter) { + // $context = ['operation' => $operation, 'uri_variables_map' => $uriVariablesMap]; + // $identifiers = $this->uriVariablesConverter->convert($identifiers, $operation->getClass() ?? $resourceClass, $context); + // } + + $context = [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + ]; + + if (null === $operation->canValidate()) { + $operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE')); + } + + if (null === $operation->canRead()) { + $operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe()); // @phpstan-ignore-line + } + + if (null === $operation->canDeserialize()) { + $operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true)); // @phpstan-ignore-line + } + + $body = $this->provider->provide($operation, $uriVariables, $context); + + // The provider can change the Operation, extract it again from the Request attributes + if ($request->attributes->get('_api_operation') !== $operation) { + $operation = $request->attributes->get('_api_operation'); + $uriVariables = $this->getUriVariables($request, $operation); + } + + $context['previous_data'] = $request->attributes->get('previous_data'); + $context['data'] = $request->attributes->get('data'); + + if (null === $operation->canWrite()) { + $operation = $operation->withWrite(!$request->isMethodSafe()); + } + + if (null === $operation->canSerialize()) { + $operation = $operation->withSerialize(true); + } + + return $this->processor->process($body, $operation, $uriVariables, $context); + } + + /** + * @return array + */ + private function getUriVariables(Request $request, HttpOperation $operation): array + { + $uriVariables = []; + foreach ($operation->getUriVariables() ?? [] as $parameterName => $_) { + $parameter = $request->route($parameterName); + if (\is_string($parameter) && ($format = $request->attributes->get('_format')) && str_contains($parameter, $format)) { + $parameter = substr($parameter, 0, \strlen($parameter) - (\strlen($format) + 1)); + } + + $uriVariables[(string) $parameterName] = $parameter; + } + + return $uriVariables; + } +} diff --git a/src/Laravel/Controller/DocumentationController.php b/src/Laravel/Controller/DocumentationController.php new file mode 100644 index 00000000000..0b3b1809b74 --- /dev/null +++ b/src/Laravel/Controller/DocumentationController.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer; +use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; +use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Generates the API documentation. + * + * @author Amrouche Hamza + */ +final class DocumentationController +{ + use ContentNegotiationTrait; + + /** + * @param array $documentationFormats + * @param ProviderInterface $provider + * @param ProcessorInterface $processor + */ + public function __construct( + private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + private readonly string $title = '', + private readonly string $description = '', + private readonly string $version = '', + private readonly ?OpenApiFactoryInterface $openApiFactory = null, + private readonly ?ProviderInterface $provider = null, + private readonly ?ProcessorInterface $processor = null, + ?Negotiator $negotiator = null, + private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']], + private readonly bool $swaggerUiEnabled = true, + ) { + $this->negotiator = $negotiator ?? new Negotiator(); + } + + public function __invoke(Request $request): Response + { + $context = [ + 'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), + 'base_url' => $request->getBaseUrl(), + 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), + ]; + $request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context); + // We want to find the format early on, this code is also executed later on by the ContentNegotiationProvider. + $this->addRequestFormats($request, $this->documentationFormats); + $format = $this->getRequestFormat($request, $this->documentationFormats); + + if ('html' === $format || OpenApiNormalizer::FORMAT === $format || OpenApiNormalizer::JSON_FORMAT === $format || OpenApiNormalizer::YAML_FORMAT === $format) { + return $this->getOpenApiDocumentation($context, $format, $request); + } + + return $this->getHydraDocumentation($context, $request); + } + + /** + * @param array $context + */ + private function getOpenApiDocumentation(array $context, string $format, Request $request): Response + { + $context['request'] = $request; + $operation = new Get( + class: OpenApi::class, + read: true, + serialize: true, + provider: fn () => $this->openApiFactory->__invoke($context), + normalizationContext: [ + ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null, + LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null, + ], + outputFormats: $this->documentationFormats + ); + + if ('html' === $format && $this->swaggerUiEnabled) { + $operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true); + } + + return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); + } + + /** + * TODO: the logic behind the Hydra Documentation is done in a ApiPlatform\Hydra\Serializer\DocumentationNormalizer. + * We should transform this to a provider, it'd improve performances also by a bit. + * + * @param array $context + */ + private function getHydraDocumentation(array $context, Request $request): Response + { + $context['request'] = $request; + $operation = new Get( + class: Documentation::class, + read: true, + serialize: true, + provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version) + ); + + return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); + } +} diff --git a/src/Laravel/Controller/EntrypointController.php b/src/Laravel/Controller/EntrypointController.php new file mode 100644 index 00000000000..d013352c0b6 --- /dev/null +++ b/src/Laravel/Controller/EntrypointController.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\Documentation\Entrypoint; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Generates the API entrypoint. + * + * @author Kévin Dunglas + */ +final class EntrypointController +{ + private static ResourceNameCollection $resourceNameCollection; + + /** + * @param array $documentationFormats + * @param ProviderInterface $provider + * @param ProcessorInterface $processor + */ + public function __construct( + private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + private readonly ProviderInterface $provider, + private readonly ProcessorInterface $processor, + private readonly array $documentationFormats = [], + ) { + } + + public function __invoke(Request $request): Response + { + self::$resourceNameCollection = $this->resourceNameCollectionFactory->create(); + $context = [ + 'request' => $request, + 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), + ]; + $request->attributes->set('_api_platform_disable_listeners', true); + $operation = new Get( + outputFormats: $this->documentationFormats, + read: true, + serialize: true, + class: Entrypoint::class, + provider: [self::class, 'provide'] + ); + $request->attributes->set('_api_operation', $operation); + $body = $this->provider->provide($operation, [], $context); + $operation = $request->attributes->get('_api_operation'); + + return $this->processor->process($body, $operation, [], $context); + } + + public static function provide(): Entrypoint + { + return new Entrypoint(self::$resourceNameCollection); + } +} diff --git a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php new file mode 100644 index 00000000000..a02effd5b71 --- /dev/null +++ b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Extension; + +use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ParameterNotFound; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +final readonly class FilterQueryExtension implements QueryExtensionInterface +{ + public function __construct( + private ContainerInterface $filterLocator, + ) { + } + + /** + * @param Builder $builder + * @param array $uriVariables + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder + { + $context['uri_variables'] = $uriVariables; + $context['operation'] = $operation; + + foreach ($operation->getParameters() ?? [] as $parameter) { + if (!($values = $parameter->getValue()) || $values instanceof ParameterNotFound) { + continue; + } + + if (null === ($filterId = $parameter->getFilter())) { + continue; + } + + $filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null); + if ($filter instanceof FilterInterface) { + $builder = $filter->apply($builder, $values, $parameter, $context + ($parameter->getFilterContext() ?? [])); + } + } + + return $builder; + } +} diff --git a/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php b/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php new file mode 100644 index 00000000000..1b36f27dbf5 --- /dev/null +++ b/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Extension; + +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +interface QueryExtensionInterface +{ + /** + * @param Builder $builder + * @param array $uriVariables + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder; +} diff --git a/src/Laravel/Eloquent/Filter/DateFilter.php b/src/Laravel/Eloquent/Filter/DateFilter.php new file mode 100644 index 00000000000..67665dce87f --- /dev/null +++ b/src/Laravel/Eloquent/Filter/DateFilter.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class DateFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + private const OPERATOR_VALUE = [ + 'eq' => '=', + 'gt' => '>', + 'lt' => '<', + 'gte' => '>=', + 'lte' => '<=', + ]; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_array($values)) { + return $builder; + } + + $values = array_intersect_key($values, self::OPERATOR_VALUE); + + if (!$values) { + return $builder; + } + + if (true === ($parameter->getFilterContext()['include_nulls'] ?? false)) { + foreach ($values as $key => $value) { + $datetime = $this->getDateTime($value); + if (null === $datetime) { + continue; + } + $builder->{$context['whereClause'] ?? 'where'}(function (Builder $query) use ($parameter, $datetime, $key): void { + $queryProperty = $this->getQueryProperty($parameter); + $query->whereDate($queryProperty, self::OPERATOR_VALUE[$key], $datetime) + ->orWhereNull($queryProperty); + }); + } + + return $builder; + } + + foreach ($values as $key => $value) { + $datetime = $this->getDateTime($value); + if (null === $datetime) { + continue; + } + $builder = $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), self::OPERATOR_VALUE[$key], $datetime); + } + + return $builder; + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'date']; + } + + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[eq]', in: $in), + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } + + private function getDateTime(string $value): ?\DateTimeImmutable + { + try { + return new \DateTimeImmutable($value); + } catch (\DateMalformedStringException|\Exception) { + return null; + } + } +} diff --git a/src/Laravel/Eloquent/Filter/EndSearchFilter.php b/src/Laravel/Eloquent/Filter/EndSearchFilter.php new file mode 100644 index 00000000000..7b70210d1ee --- /dev/null +++ b/src/Laravel/Eloquent/Filter/EndSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class EndSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', '%'.$values); + } +} diff --git a/src/Laravel/Eloquent/Filter/EqualsFilter.php b/src/Laravel/Eloquent/Filter/EqualsFilter.php new file mode 100644 index 00000000000..d95d13d6751 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/EqualsFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class EqualsFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), $values); + } +} diff --git a/src/Laravel/Eloquent/Filter/FilterInterface.php b/src/Laravel/Eloquent/Filter/FilterInterface.php new file mode 100644 index 00000000000..b3233b99830 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/FilterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +interface FilterInterface +{ + /** + * @param Builder $builder + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder; +} diff --git a/src/Laravel/Eloquent/Filter/OrFilter.php b/src/Laravel/Eloquent/Filter/OrFilter.php new file mode 100644 index 00000000000..6c9e7f833b2 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/OrFilter.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final readonly class OrFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + public function __construct(private FilterInterface $filter) + { + } + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->where(function ($builder) use ($values, $parameter, $context): void { + foreach ($values as $value) { + $this->filter->apply($builder, $value, $parameter, ['whereClause' => 'orWhere'] + $context); + } + }); + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + $schema = $this->filter instanceof JsonSchemaFilterInterface ? $this->filter->getSchema($parameter) : ['type' => 'string']; + + return ['type' => 'array', 'items' => $schema]; + } + + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); + } +} diff --git a/src/Laravel/Eloquent/Filter/OrderFilter.php b/src/Laravel/Eloquent/Filter/OrderFilter.php new file mode 100644 index 00000000000..90315f3d083 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/OrderFilter.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_string($values)) { + $properties = $parameter->getExtraProperties()['_properties'] ?? []; + + foreach ($values as $key => $value) { + if (!isset($properties[$key])) { + continue; + } + $builder = $builder->orderBy($properties[$key], $value); + } + + return $builder; + } + + return $builder->orderBy($this->getQueryProperty($parameter), $values); + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'enum' => ['asc', 'desc']]; + } + + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + if (str_contains($parameter->getKey(), ':property')) { + $parameters = []; + $key = str_replace('[:property]', '', $parameter->getKey()); + foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) { + $parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query'); + } + + return $parameters; + } + + return null; + } +} diff --git a/src/Laravel/Eloquent/Filter/PartialSearchFilter.php b/src/Laravel/Eloquent/Filter/PartialSearchFilter.php new file mode 100644 index 00000000000..70c03abc62d --- /dev/null +++ b/src/Laravel/Eloquent/Filter/PartialSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class PartialSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', '%'.$values.'%'); + } +} diff --git a/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php b/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php new file mode 100644 index 00000000000..3cee8cdc372 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; + +/** + * @internal + */ +trait QueryPropertyTrait +{ + private function getQueryProperty(Parameter $parameter): ?string + { + return $parameter->getExtraProperties()['_query_property'] ?? $parameter->getProperty() ?? null; + } +} diff --git a/src/Laravel/Eloquent/Filter/RangeFilter.php b/src/Laravel/Eloquent/Filter/RangeFilter.php new file mode 100644 index 00000000000..82f7e4457ec --- /dev/null +++ b/src/Laravel/Eloquent/Filter/RangeFilter.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class RangeFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + private const OPERATOR_VALUE = [ + 'lt' => '<', + 'gt' => '>', + 'lte' => '<=', + 'gte' => '>=', + ]; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + $queryProperty = $this->getQueryProperty($parameter); + + foreach ($values as $key => $value) { + $builder = $builder->{$context['whereClause'] ?? 'where'}($queryProperty, self::OPERATOR_VALUE[$key], $value); + } + + return $builder; + } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'number']; + } + + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } +} diff --git a/src/Laravel/Eloquent/Filter/StartSearchFilter.php b/src/Laravel/Eloquent/Filter/StartSearchFilter.php new file mode 100644 index 00000000000..b0b20a56b0c --- /dev/null +++ b/src/Laravel/Eloquent/Filter/StartSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class StartSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', $values.'%'); + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php new file mode 100644 index 00000000000..439ef2db49b --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Database\Eloquent\Model; + +/** + * Handles Eloquent methods for relations. + */ +final class EloquentAttributePropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private readonly ?PropertyMetadataFactoryInterface $decorated = null, + ) { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + if (!class_exists($resourceClass)) { + return $this->decorated?->create($resourceClass, $property, $options) ?? + $this->throwNotFound($resourceClass, $property); + } + + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + + $propertyMetadata = $this->decorated?->create($resourceClass, $property, $options); + if (!$model instanceof Model) { + return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property); + } + + if ($refl->hasMethod($property) && $attributes = $refl->getMethod($property)->getAttributes(ApiProperty::class)) { + return $this->createMetadata($attributes[0]->newInstance(), $propertyMetadata); + } + + return $propertyMetadata; + } + + /** + * @throws PropertyNotFoundException + */ + private function throwNotFound(string $resourceClass, string $property): never + { + throw new PropertyNotFoundException(\sprintf('Property "%s" of class "%s" not found.', $property, $resourceClass)); + } + + private function createMetadata(ApiProperty $attribute, ?ApiProperty $propertyMetadata = null): ApiProperty + { + if (null === $propertyMetadata) { + return $this->handleUserDefinedSchema($attribute); + } + + foreach (get_class_methods(ApiProperty::class) as $method) { + if (preg_match('/^(?:get|is)(.*)/', $method, $matches) && null !== $val = $attribute->{$method}()) { + $propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val); + } + } + + return $this->handleUserDefinedSchema($propertyMetadata); + } + + private function handleUserDefinedSchema(ApiProperty $propertyMetadata): ApiProperty + { + // can't know later if the schema has been defined by the user or by API Platform + // store extra key to make this difference + if (null !== $propertyMetadata->getSchema()) { + $extraProperties = $propertyMetadata->getExtraProperties() ?? []; + $propertyMetadata = $propertyMetadata->withExtraProperties([SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED => true] + $extraProperties); + } + + return $propertyMetadata; + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php new file mode 100644 index 00000000000..c88c96585e2 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Support\Collection; +use Symfony\Component\PropertyInfo\Type; + +/** + * Uses Eloquent metadata to populate the identifier property. + * + * @author Kévin Dunglas + */ +final class EloquentPropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private readonly ModelMetadata $modelMetadata, + private readonly ?PropertyMetadataFactoryInterface $decorated = null, + ) { + } + + /** + * {@inheritdoc} + * + * @param class-string $resourceClass + */ + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + try { + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } + + if (!$model instanceof Model) { + return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } + + try { + $propertyMetadata = $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } catch (PropertyNotFoundException) { + $propertyMetadata = new ApiProperty(); + } + + if ($model->getKeyName() === $property) { + $propertyMetadata = $propertyMetadata->withIdentifier(true)->withWritable($propertyMetadata->isWritable() ?? false); + } + + foreach ($this->modelMetadata->getAttributes($model) as $p) { + if ($p['name'] !== $property) { + continue; + } + + // see https://laravel.com/docs/11.x/eloquent-mutators#attribute-casting + $builtinType = $p['cast'] ?? $p['type']; + $type = match ($builtinType) { + 'integer' => new Type(Type::BUILTIN_TYPE_INT, $p['nullable']), + 'double', 'real' => new Type(Type::BUILTIN_TYPE_FLOAT, $p['nullable']), + 'datetime', 'date', 'timestamp' => new Type(Type::BUILTIN_TYPE_OBJECT, $p['nullable'], \DateTime::class), + 'immutable_datetime', 'immutable_date' => new Type(Type::BUILTIN_TYPE_OBJECT, $p['nullable'], \DateTimeImmutable::class), + 'collection', 'encrypted:collection' => new Type(Type::BUILTIN_TYPE_ITERABLE, $p['nullable'], Collection::class, true), + 'encrypted:array' => new Type(Type::BUILTIN_TYPE_ARRAY, $p['nullable']), + 'encrypted:object' => new Type(Type::BUILTIN_TYPE_OBJECT, $p['nullable']), + default => new Type(\in_array($builtinType, Type::$builtinTypes, true) ? $builtinType : Type::BUILTIN_TYPE_STRING, $p['nullable'] ?? true), + }; + + return $propertyMetadata + ->withBuiltinTypes([$type]) + ->withWritable($propertyMetadata->isWritable() ?? true) + ->withReadable($propertyMetadata->isReadable() ?? false === $p['hidden']); + } + + foreach ($this->modelMetadata->getRelations($model) as $relation) { + if ($relation['name'] !== $property) { + continue; + } + + $collection = match ($relation['type']) { + HasMany::class, + HasManyThrough::class, + BelongsToMany::class, + MorphMany::class, + MorphToMany::class => true, + default => false, + }; + + $type = new Type($collection ? Type::BUILTIN_TYPE_ITERABLE : Type::BUILTIN_TYPE_OBJECT, false, $relation['related'], $collection, collectionValueType: new Type(Type::BUILTIN_TYPE_OBJECT, false, $relation['related'])); + + return $propertyMetadata + ->withBuiltinTypes([$type]) + ->withWritable($propertyMetadata->isWritable() ?? true) + ->withReadable($propertyMetadata->isReadable() ?? true) + ->withExtraProperties(['eloquent_relation' => $relation] + $propertyMetadata->getExtraProperties()); + } + + return $propertyMetadata; + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php new file mode 100644 index 00000000000..2db75b3972c --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Database\Eloquent\Model; + +final class EloquentPropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct( + private readonly ModelMetadata $modelMetadata, + private readonly ?PropertyNameCollectionFactoryInterface $decorated, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ) { + } + + /** + * {@inheritdoc} + * + * @param class-string $resourceClass + */ + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + if (!class_exists($resourceClass) || !is_a($resourceClass, Model::class, true)) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + try { + $refl = new \ReflectionClass($resourceClass); + if ($refl->isAbstract()) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + /** + * @var array $properties + */ + $properties = []; + + // When it's an Eloquent model we read attributes from database (@see ShowModelCommand) + foreach ($this->modelMetadata->getAttributes($model) as $property) { + if (!($property['primary'] ?? null) && $property['hidden']) { + continue; + } + + $properties[$property['name']] = true; + } + + foreach ($this->modelMetadata->getRelations($model) as $relation) { + if (!$this->resourceClassResolver->isResourceClass($relation['related'])) { + continue; + } + + $properties[$relation['name']] = true; + } + + return new PropertyNameCollection( + array_keys($properties) // @phpstan-ignore-line + ); + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php new file mode 100644 index 00000000000..551a08576aa --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource; + +use ApiPlatform\Laravel\Eloquent\State\CollectionProvider; +use ApiPlatform\Laravel\Eloquent\State\ItemProvider; +use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; +use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\DeleteOperationInterface; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Gate; + +final class EloquentResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + private const POLICY_METHODS = [ + Put::class => 'update', + Post::class => 'create', + Get::class => 'view', + GetCollection::class => 'viewAny', + Delete::class => 'delete', + Patch::class => 'update', + + Query::class => 'view', + QueryCollection::class => 'viewAny', + Mutation::class => 'update', + DeleteMutation::class => 'delete', + Subscription::class => 'viewAny', + ]; + + public function __construct( + private readonly ResourceMetadataCollectionFactoryInterface $decorated, + ) { + } + + /** + * @param class-string $resourceClass + */ + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + + try { + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated->create($resourceClass); + } + + if (!$model instanceof Model) { + return $resourceMetadataCollection; + } + + foreach ($resourceMetadataCollection as $i => $resourceMetadata) { + $operations = $resourceMetadata->getOperations(); + foreach ($operations ?? [] as $operationName => $operation) { + if (!$operation->getProvider()) { + $operation = $operation->withProvider($operation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class); + } + + if (!$operation->getPolicy() && ($policy = Gate::getPolicyFor($model))) { + $policyMethod = self::POLICY_METHODS[$operation::class] ?? null; + if ($operation instanceof Put && $operation->getAllowCreate()) { + $policyMethod = self::POLICY_METHODS[Post::class]; + } + + if ($policyMethod && method_exists($policy, $policyMethod)) { + $operation = $operation->withPolicy($policyMethod); + } + } + + if (!$operation->getProcessor()) { + $operation = $operation->withProcessor($operation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class); + } + + $operations->add($operationName, $operation); + } + + $resourceMetadataCollection[$i] = $resourceMetadata->withOperations($operations); + + $graphQlOperations = $resourceMetadata->getGraphQlOperations(); + foreach ($graphQlOperations ?? [] as $operationName => $graphQlOperation) { + if (!$graphQlOperation->getPolicy() && ($policy = Gate::getPolicyFor($model))) { + if (($policyMethod = self::POLICY_METHODS[$graphQlOperation::class] ?? null) && method_exists($policy, $policyMethod)) { + $graphQlOperation = $graphQlOperation->withPolicy($policyMethod); + } + } + + if (!$graphQlOperation->getProvider()) { + $graphQlOperation = $graphQlOperation->withProvider($graphQlOperation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class); + } + + if (!$graphQlOperation->getProcessor()) { + $graphQlOperation = $graphQlOperation->withProcessor($graphQlOperation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class); + } + + $graphQlOperations[$operationName] = $graphQlOperation; + } + + if ($graphQlOperations) { + $resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations); + } + + $resourceMetadataCollection[$i] = $resourceMetadata; + } + + return $resourceMetadataCollection; + } +} diff --git a/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php b/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php new file mode 100644 index 00000000000..107ef364503 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +final class IdentifiersExtractor implements IdentifiersExtractorInterface +{ + public function __construct( + private readonly IdentifiersExtractorInterface $inner, + ) { + } + + public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array + { + if (!($item instanceof BelongsTo || $item instanceof Model) || !$operation instanceof HttpOperation) { + return $this->inner->getIdentifiersFromItem($item, $operation, $context); + } + + $identifiers = []; + foreach ($operation->getUriVariables() ?? [] as $link) { + $parameterName = $link->getParameterName(); + $identifiers[$parameterName] = $this->getIdentifierValue($item, $link); + } + + return $identifiers; + } + + private function getIdentifierValue(object $item, Link $link): mixed + { + if ($item instanceof ($link->getFromClass())) { + return $this->getEloquentProperty($item, $link->getIdentifiers()[0]); + } + + if ($item instanceof BelongsTo) { + return $this->getEloquentProperty($item->getParent(), $item->getForeignKeyName()); + } + + if ($toProperty = $link->getToProperty()) { + $relation = $this->getEloquentProperty($item, $toProperty); + + if ($relation instanceof BelongsTo) { + return $this->getEloquentProperty($item, $relation->getForeignKeyName()); + } + } + + return $this->getEloquentProperty($item, $link->getIdentifiers()[0]); + } + + private function getEloquentProperty(object $item, string $property): mixed + { + if (method_exists($item, $property)) { + return $item->{$property}(); + } + + $getter = 'get'.ucfirst($property); + if (method_exists($item, $getter)) { + return $item->{$getter}(); + } + + return $item->{$property}; + } +} diff --git a/src/Laravel/Eloquent/Metadata/ModelMetadata.php b/src/Laravel/Eloquent/Metadata/ModelMetadata.php new file mode 100644 index 00000000000..3d851681c03 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/ModelMetadata.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; + +/** + * Inspired from Illuminate\Database\Console\ShowModelCommand. + * + * @internal + */ +final class ModelMetadata +{ + /** + * The methods that can be called in a model to indicate a relation. + * + * @var string[] + */ + public const RELATION_METHODS = [ + 'hasMany', + 'hasManyThrough', + 'hasOneThrough', + 'belongsToMany', + 'hasOne', + 'belongsTo', + 'morphOne', + 'morphTo', + 'morphMany', + 'morphToMany', + 'morphedByMany', + ]; + + /** + * Gets the first policy associated with this model. + */ + public function getPolicy(Model $model): ?string + { + $policy = Gate::getPolicyFor($model::class); + + return $policy ? $policy::class : null; + } + + /** + * Gets the column attributes for the given model. + * + * @return Collection + */ + public function getAttributes(Model $model): Collection + { + $connection = $model->getConnection(); + $schema = $connection->getSchemaBuilder(); + $table = $model->getTable(); + $columns = $schema->getColumns($table); + $indexes = $schema->getIndexes($table); + + return collect($columns) + ->map(fn ($column) => [ + 'name' => $column['name'], + 'type' => $column['type'], + 'increments' => $column['auto_increment'], + 'nullable' => $column['nullable'], + 'default' => $this->getColumnDefault($column, $model), + 'unique' => $this->columnIsUnique($column['name'], $indexes), + 'fillable' => $model->isFillable($column['name']), + 'hidden' => $this->attributeIsHidden($column['name'], $model), + 'appended' => null, + 'cast' => $this->getCastType($column['name'], $model), + 'primary' => $this->isColumnPrimaryKey($indexes, $column['name']), + ]) + ->merge($this->getVirtualAttributes($model, $columns)); + } + + /** + * @param array $indexes + */ + private function isColumnPrimaryKey(array $indexes, string $column): bool + { + foreach ($indexes as $index) { + if (\in_array($column, $index['columns'], true)) { + return true; + } + } + + return false; + } + + /** + * Get the virtual (non-column) attributes for the given model. + * + * @param array $columns + * + * @return Collection + */ + public function getVirtualAttributes(Model $model, array $columns): Collection + { + $class = new \ReflectionClass($model); + + return collect($class->getMethods()) + ->reject( + fn (\ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || Model::class === $method->getDeclaringClass()->getName() + ) + ->mapWithKeys(function (\ReflectionMethod $method) use ($model) { + if (1 === preg_match('/^get(.+)Attribute$/', $method->getName(), $matches)) { + return [Str::snake($matches[1]) => 'accessor']; + } + if ($model->hasAttributeMutator($method->getName())) { + return [Str::snake($method->getName()) => 'attribute']; + } + + return []; + }) + ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name)) + ->map(fn ($cast, $name) => [ + 'name' => $name, + 'type' => null, + 'increments' => false, + 'nullable' => null, + 'default' => null, + 'unique' => null, + 'fillable' => $model->isFillable($name), + 'hidden' => $this->attributeIsHidden($name, $model), + 'appended' => $model->hasAppended($name), + 'cast' => $cast, + ]) + ->values(); + } + + /** + * Gets the relations from the given model. + * + * @return Collection + */ + public function getRelations(Model $model): Collection + { + return collect(get_class_methods($model)) + ->map(fn ($method) => new \ReflectionMethod($model, $method)) + ->reject( + fn (\ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || Model::class === $method->getDeclaringClass()->getName() + || $method->getNumberOfParameters() > 0 + || $this->attributeIsHidden($method->getName(), $model) + ) + ->filter(function (\ReflectionMethod $method) { + if ($method->getReturnType() instanceof \ReflectionNamedType + && is_subclass_of($method->getReturnType()->getName(), Relation::class)) { + return true; + } + + if (false === $method->getFileName()) { + return false; + } + + $file = new \SplFileObject($method->getFileName()); + $file->seek($method->getStartLine() - 1); + $code = ''; + while ($file->key() < $method->getEndLine()) { + $current = $file->current(); + if (\is_string($current)) { + $code .= trim($current); + } + + $file->next(); + } + + return collect(self::RELATION_METHODS) + ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); + }) + ->map(function (\ReflectionMethod $method) use ($model) { + $relation = $method->invoke($model); + + if (!$relation instanceof Relation) { + return null; + } + + return [ + 'name' => $method->getName(), + 'type' => $relation::class, + 'related' => \get_class($relation->getRelated()), + 'foreign_key' => method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : null, + ]; + }) + ->filter() + ->values(); + } + + /** + * Gets the Events that the model dispatches. + * + * @return Collection + */ + public function getEvents(Model $model): Collection + { + return collect($model->dispatchesEvents()) + ->map(fn (string $class, string $event) => [ + 'event' => $event, + 'class' => $class, + ])->values(); + } + + /** + * Gets the cast type for the given column. + */ + private function getCastType(string $column, Model $model): ?string + { + if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) { + return 'accessor'; + } + + if ($model->hasAttributeMutator($column)) { + return 'attribute'; + } + + return $this->getCastsWithDates($model)->get($column) ?? null; + } + + /** + * Gets the model casts, including any date casts. + * + * @return Collection + */ + private function getCastsWithDates(Model $model): Collection + { + return collect($model->getDates()) + ->filter() + ->flip() + ->map(fn () => 'datetime') + ->merge($model->getCasts()); + } + + /** + * Gets the default value for the given column. + * + * @param array&array{name: string, default: string} $column + */ + private function getColumnDefault(array $column, Model $model): mixed + { + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; + + return match (true) { + $attributeDefault instanceof \BackedEnum => $attributeDefault->value, + $attributeDefault instanceof \UnitEnum => $attributeDefault->name, + default => $attributeDefault ?? $column['default'], + }; + } + + /** + * Determines if the given attribute is hidden. + */ + private function attributeIsHidden(string $attribute, Model $model): bool + { + if ($visible = $model->getVisible()) { + return !\in_array($attribute, $visible, true); + } + + if ($hidden = $model->getHidden()) { + return \in_array($attribute, $hidden, true); + } + + return false; + } + + /** + * Determines if the given attribute is unique. + * + * @param array $indexes + */ + private function columnIsUnique(string $column, array $indexes): bool + { + return collect($indexes)->contains( + fn ($index) => 1 === \count($index['columns']) && $index['columns'][0] === $column && $index['unique'] + ); + } +} diff --git a/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php b/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php new file mode 100644 index 00000000000..69aa2f948b0 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Database\Eloquent\Relations\Relation; + +final class ResourceClassResolver implements ResourceClassResolverInterface +{ + public function __construct( + private readonly ResourceClassResolverInterface $inner, + ) { + } + + public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string + { + if ($value instanceof Relation) { + return $this->inner->getResourceClass($value->getRelated()); + } + + return $this->inner->getResourceClass($value, $resourceClass, $strict); + } + + public function isResourceClass(string $type): bool + { + return $this->inner->isResourceClass($type); + } +} diff --git a/src/Laravel/Eloquent/Paginator.php b/src/Laravel/Eloquent/Paginator.php new file mode 100644 index 00000000000..a467d46679f --- /dev/null +++ b/src/Laravel/Eloquent/Paginator.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent; + +use ApiPlatform\State\Pagination\PaginatorInterface; +use Illuminate\Pagination\LengthAwarePaginator; +use IteratorAggregate; + +/** + * @implements IteratorAggregate + * @implements PaginatorInterface + */ +final class Paginator implements PaginatorInterface, \IteratorAggregate +{ + /** + * @param LengthAwarePaginator $paginator + */ + public function __construct( + private readonly LengthAwarePaginator $paginator, + ) { + } + + public function count(): int + { + return $this->paginator->count(); + } + + public function getLastPage(): float + { + return $this->paginator->lastPage(); + } + + public function getTotalItems(): float + { + return $this->paginator->total(); + } + + public function getCurrentPage(): float + { + return $this->paginator->currentPage(); + } + + public function getItemsPerPage(): float + { + return $this->paginator->perPage(); + } + + public function getIterator(): \Traversable + { + return $this->paginator->getIterator(); + } +} diff --git a/src/Laravel/Eloquent/PartialPaginator.php b/src/Laravel/Eloquent/PartialPaginator.php new file mode 100644 index 00000000000..e8dd57aae8f --- /dev/null +++ b/src/Laravel/Eloquent/PartialPaginator.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent; + +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use Illuminate\Pagination\AbstractPaginator; +use IteratorAggregate; + +/** + * @implements IteratorAggregate + * @implements PartialPaginatorInterface + */ +final class PartialPaginator implements PartialPaginatorInterface, \IteratorAggregate +{ + /** + * @param AbstractPaginator $paginator + */ + public function __construct( + private readonly AbstractPaginator $paginator, + ) { + } + + public function count(): int + { + return $this->paginator->count(); + } + + public function getCurrentPage(): float + { + return $this->paginator->currentPage(); + } + + public function getItemsPerPage(): float + { + return $this->paginator->perPage(); + } + + public function getIterator(): \Traversable + { + return $this->paginator->getIterator(); + } +} diff --git a/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php b/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php new file mode 100644 index 00000000000..8f20321d4ab --- /dev/null +++ b/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\PropertyAccess; + +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * @internal + */ +final class PropertyAccessor implements PropertyAccessorInterface +{ + private readonly PropertyAccessorInterface $inner; + + public function __construct( + ?PropertyAccessorInterface $inner = null, + ) { + $this->inner = $inner ?? PropertyAccess::createPropertyAccessor(); + } + + /** + * @param array|object $objectOrArray + */ + public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void + { + if ($objectOrArray instanceof Model) { + $objectOrArray->{$propertyPath} = $value; + + return; + } + + $this->inner->setValue($objectOrArray, $propertyPath, $value); + } + + /** + * @param array|object $objectOrArray + */ + public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed + { + if ($objectOrArray instanceof Model) { + return $objectOrArray->{$propertyPath}; + } + + return $this->inner->getValue($objectOrArray, $propertyPath); + } + + /** + * @param array|object $objectOrArray + */ + public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + if ($objectOrArray instanceof Model) { + return true; + } + + return $this->inner->isWritable($objectOrArray, $propertyPath); + } + + /** + * @param array|object $objectOrArray + */ + public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + if ($objectOrArray instanceof Model) { + return true; + } + + return $this->inner->isReadable($objectOrArray, $propertyPath); + } +} diff --git a/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php b/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php new file mode 100644 index 00000000000..d111f8ce1d2 --- /dev/null +++ b/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Serializer; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +final class SerializerContextBuilder implements SerializerContextBuilderInterface +{ + public function __construct( + private readonly SerializerContextBuilderInterface $decorated, + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + ) { + } + + /** + * @param array $extractedAttributes + */ + public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array + { + $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); + if (!isset($context['resource_class']) || !is_a($context['resource_class'], Model::class, true)) { + return $context; + } + + if (!isset($context[AbstractNormalizer::ATTRIBUTES])) { + // isWritable/isReadable is checked later on + $context[AbstractNormalizer::ATTRIBUTES] = iterator_to_array($this->propertyNameCollectionFactory->create($context['resource_class'], ['serializer_groups' => $context['groups'] ?? null])); + } + + return $context; + } +} diff --git a/src/Laravel/Eloquent/Serializer/SnakeCaseToCamelCaseNameConverter.php b/src/Laravel/Eloquent/Serializer/SnakeCaseToCamelCaseNameConverter.php new file mode 100644 index 00000000000..11b8862d9f4 --- /dev/null +++ b/src/Laravel/Eloquent/Serializer/SnakeCaseToCamelCaseNameConverter.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Serializer; + +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Underscore to cameCase name converter. + * + * @internal + * + * @see Adapted from https://github.com/symfony/symfony/blob/7.2/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php. + * + * @author Kévin Dunglas + * @author Aurélien Pillevesse + * @copyright Fabien Potencier + */ +final class SnakeCaseToCamelCaseNameConverter implements NameConverterInterface +{ + /** + * @param string[]|null $attributes The list of attributes to rename or null for all attributes + */ + public function __construct( + private readonly ?array $attributes = null, + ) { + } + + /** + * @param class-string|null $class + * @param array $context + */ + public function normalize( + string $propertyName, ?string $class = null, ?string $format = null, array $context = [], + ): string { + if (null === $this->attributes || \in_array($propertyName, $this->attributes, true)) { + return lcfirst(preg_replace_callback( + '/(^|_|\.)+(.)/', + fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), + $propertyName + )); + } + + return $propertyName; + } + + /** + * @param class-string|null $class + * @param array $context + */ + public function denormalize( + string $propertyName, ?string $class = null, ?string $format = null, array $context = [], + ): string { + $snakeCased = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName))); + if (null === $this->attributes || \in_array($snakeCased, $this->attributes, true)) { + return $snakeCased; + } + + return $propertyName; + } +} diff --git a/src/Laravel/Eloquent/State/CollectionProvider.php b/src/Laravel/Eloquent/State/CollectionProvider.php new file mode 100644 index 00000000000..2557dd842c9 --- /dev/null +++ b/src/Laravel/Eloquent/State/CollectionProvider.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Laravel\Eloquent\Paginator; +use ApiPlatform\Laravel\Eloquent\PartialPaginator; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +/** + * @implements ProviderInterface|PartialPaginator> + */ +final class CollectionProvider implements ProviderInterface +{ + use LinksHandlerLocatorTrait; + + /** + * @param LinksHandlerInterface $linksHandler + * @param iterable $queryExtensions + */ + public function __construct( + private readonly Pagination $pagination, + private readonly LinksHandlerInterface $linksHandler, + private iterable $queryExtensions = [], + ?ContainerInterface $handleLinksLocator = null, + ) { + $this->handleLinksLocator = $handleLinksLocator; + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + /** @var Model $model */ + $model = new ($operation->getClass())(); + + if ($handleLinks = $this->getLinksHandler($operation)) { + $query = $handleLinks($model->query(), $uriVariables, ['operation' => $operation, 'modelClass' => $operation->getClass()] + $context); + } else { + $query = $this->linksHandler->handleLinks($model->query(), $uriVariables, ['operation' => $operation, 'modelClass' => $operation->getClass()] + $context); + } + + foreach ($this->queryExtensions as $extension) { + $query = $extension->apply($query, $uriVariables, $operation, $context); + } + + if (false === $this->pagination->isEnabled($operation, $context)) { + return $query->get(); + } + + $isPartial = $operation->getPaginationPartial(); + $collection = $query + ->{$isPartial ? 'simplePaginate' : 'paginate'}( + perPage: $this->pagination->getLimit($operation, $context), + page: $this->pagination->getPage($context), + ); + + if ($isPartial) { + return new PartialPaginator($collection); + } + + return new Paginator($collection); + } +} diff --git a/src/Laravel/Eloquent/State/ItemProvider.php b/src/Laravel/Eloquent/State/ItemProvider.php new file mode 100644 index 00000000000..43a7bfe6f7a --- /dev/null +++ b/src/Laravel/Eloquent/State/ItemProvider.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +/** + * @implements ProviderInterface + */ +final class ItemProvider implements ProviderInterface +{ + use LinksHandlerLocatorTrait; + + /** + * @param LinksHandlerInterface $linksHandler + */ + public function __construct( + private readonly LinksHandlerInterface $linksHandler, + ?ContainerInterface $handleLinksLocator = null, + ) { + $this->handleLinksLocator = $handleLinksLocator; + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $model = new ($operation->getClass())(); + + if ($handleLinks = $this->getLinksHandler($operation)) { + $query = $handleLinks($model->query(), $uriVariables, ['operation' => $operation] + $context); + } else { + $query = $this->linksHandler->handleLinks($model->query(), $uriVariables, ['operation' => $operation] + $context); + } + + return $query->first(); + } +} diff --git a/src/Laravel/Eloquent/State/LinksHandler.php b/src/Laravel/Eloquent/State/LinksHandler.php new file mode 100644 index 00000000000..32256ab717c --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandler.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\HttpOperation; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +/** + * @implements LinksHandlerInterface + */ +final class LinksHandler implements LinksHandlerInterface +{ + public function __construct( + private readonly Application $application, + ) { + } + + public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder + { + $operation = $context['operation']; + + if ($operation instanceof HttpOperation) { + foreach (array_reverse($operation->getUriVariables() ?? []) as $uriVariable => $link) { + $identifier = $uriVariables[$uriVariable]; + + if ($to = $link->getToProperty()) { + $builder = $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier); + + continue; + } + + if ($from = $link->getFromProperty()) { + $relation = $this->application->make($link->getFromClass()); + $builder = $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier); + + continue; + } + + $builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier); + } + + return $builder; + } + + return $builder; + } +} diff --git a/src/Laravel/Eloquent/State/LinksHandlerInterface.php b/src/Laravel/Eloquent/State/LinksHandlerInterface.php new file mode 100644 index 00000000000..ae3089e501c --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandlerInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +/** + * @template T of Model + */ +interface LinksHandlerInterface +{ + /** + * Handles Laravel links. + * + * @param Builder $builder + * @param array $uriVariables + * @param array{modelClass: string, operation: Operation}|array $context + * + * @return Builder + */ + public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder; +} diff --git a/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php b/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php new file mode 100644 index 00000000000..0975cf52e3f --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use Psr\Container\ContainerInterface; + +/** + * @internal + */ +trait LinksHandlerLocatorTrait +{ + private ?ContainerInterface $handleLinksLocator; + + private function getLinksHandler(Operation $operation): ?callable + { + if (!($options = $operation->getStateOptions()) || !$options instanceof Options) { + return null; + } + + $handleLinks = $options->getHandleLinks(); + if (\is_callable($handleLinks)) { + return $handleLinks; + } + + if ($this->handleLinksLocator && \is_string($handleLinks) && $this->handleLinksLocator->has($handleLinks)) { + return [$this->handleLinksLocator->get($handleLinks), 'handleLinks']; // @phpstan-ignore-line + } + + throw new RuntimeException(\sprintf('Could not find handleLinks service "%s"', $handleLinks)); + } +} diff --git a/src/Laravel/Eloquent/State/Options.php b/src/Laravel/Eloquent/State/Options.php new file mode 100644 index 00000000000..f9466c509da --- /dev/null +++ b/src/Laravel/Eloquent/State/Options.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\State\OptionsInterface; + +class Options implements OptionsInterface +{ + /** + * @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future + * + * @see LinksHandlerInterface + */ + public function __construct( + protected mixed $handleLinks = null, + ) { + } + + public function getHandleLinks(): mixed + { + return $this->handleLinks; + } + + public function withHandleLinks(mixed $handleLinks): self + { + $self = clone $this; + $self->handleLinks = $handleLinks; + + return $self; + } +} diff --git a/src/Laravel/Eloquent/State/PersistProcessor.php b/src/Laravel/Eloquent/State/PersistProcessor.php new file mode 100644 index 00000000000..84a01ce661d --- /dev/null +++ b/src/Laravel/Eloquent/State/PersistProcessor.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +/** + * @implements ProcessorInterface<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> + */ +final class PersistProcessor implements ProcessorInterface +{ + public function __construct( + private readonly ModelMetadata $modelMetadata, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + foreach ($this->modelMetadata->getRelations($data) as $relation) { + if (!isset($data->{$relation['name']})) { + continue; + } + + if (BelongsTo::class === $relation['type']) { + $data->{$relation['name']}()->associate($data->{$relation['name']}); + unset($data->{$relation['name']}); + } + } + + if (($previousData = $context['previous_data'] ?? null) && $operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true)) { + foreach ($this->modelMetadata->getAttributes($data) as $attribute) { + if ($attribute['primary'] ?? false) { + $data->{$attribute['name']} = $previousData->{$attribute['name']}; + } + } + $data->exists = true; + } + + $data->saveOrFail(); + $data->refresh(); + + return $data; + } +} diff --git a/src/Laravel/Eloquent/State/RemoveProcessor.php b/src/Laravel/Eloquent/State/RemoveProcessor.php new file mode 100644 index 00000000000..a4a4583a01b --- /dev/null +++ b/src/Laravel/Eloquent/State/RemoveProcessor.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; + +/** + * @implements ProcessorInterface<\Illuminate\Database\Eloquent\Model, null> + */ +final class RemoveProcessor implements ProcessorInterface +{ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + $data->delete(); + + return null; + } +} diff --git a/src/Laravel/Exception/ErrorHandler.php b/src/Laravel/Exception/ErrorHandler.php new file mode 100644 index 00000000000..60578341b10 --- /dev/null +++ b/src/Laravel/Exception/ErrorHandler.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Exception; + +use ApiPlatform\Laravel\ApiResource\Error; +use ApiPlatform\Laravel\Controller\ApiPlatformController; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use ApiPlatform\Metadata\Exception\StatusAwareExceptionInterface; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use ApiPlatform\State\Util\OperationRequestInitiatorTrait; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Contracts\Container\Container; +use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler; +use Illuminate\Http\Request; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + +class ErrorHandler extends ExceptionsHandler +{ + use ContentNegotiationTrait; + use OperationRequestInitiatorTrait; + + public static mixed $error; + + /** + * @param array $exceptionToStatus + */ + public function __construct( + Container $container, + ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + private readonly ApiPlatformController $apiPlatformController, + private readonly ?IdentifiersExtractorInterface $identifiersExtractor = null, + private readonly ?ResourceClassResolverInterface $resourceClassResolver = null, + ?Negotiator $negotiator = null, + private readonly ?array $exceptionToStatus = null, + private readonly ?bool $debug = false, + ) { + $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; + $this->negotiator = $negotiator; + // calls register + parent::__construct($container); + $this->register(); + } + + public function register(): void + { + $this->renderable(function (\Throwable $exception, Request $request) { + $apiOperation = $this->initializeOperation($request); + if (!$apiOperation) { + return null; + } + + $formats = config('api-platform.error_formats') ?? ['jsonproblem' => ['application/problem+json']]; + $format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false); + + if ($this->resourceClassResolver->isResourceClass($exception::class)) { + $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); + + $operation = null; + foreach ($resourceCollection as $resource) { + foreach ($resource->getOperations() as $op) { + foreach ($op->getOutputFormats() as $key => $value) { + if ($key === $format) { + $operation = $op; + break 3; + } + } + } + } + + // No operation found for the requested format, we take the first available + if (!$operation) { + $operation = $resourceCollection->getOperation(); + } + $errorResource = $exception; + if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); + if ($errorResource instanceof StatusAwareExceptionInterface) { + $errorResource->setStatus($statusCode); + } + } + } else { + // Create a generic, rfc7807 compatible error according to the wanted format + $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); + // status code may be overridden by the exceptionToStatus option + $statusCode = 500; + if ($operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); + } + + $errorResource = Error::createFromException($exception, $statusCode); + } + + /** @var HttpOperation $operation */ + if (!$operation->getProvider()) { + // TODO: validation + // static::$error = 'jsonapi' === $format && $errorResource instanceof ConstraintViolationListAwareExceptionInterface ? $errorResource->getConstraintViolationList() : $errorResource; + static::$error = $errorResource; + $operation = $operation->withProvider([self::class, 'provide']); + } + + // For our swagger Ui errors + if ('html' === $format) { + $operation = $operation->withOutputFormats(['html' => ['text/html']]); + } + + $identifiers = []; + try { + $identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? []; + } catch (\Exception $e) { + } + + $normalizationContext = $operation->getNormalizationContext() ?? []; + if (!($normalizationContext['api_error_resource'] ?? false)) { + $normalizationContext += ['api_error_resource' => true]; + } + + if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) { + $normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace']; + } + + $operation = $operation->withNormalizationContext($normalizationContext); + + $dup = $request->duplicate(null, null, []); + $dup->setMethod('GET'); + $dup->attributes->set('_api_resource_class', $operation->getClass()); + $dup->attributes->set('_api_previous_operation', $apiOperation); + $dup->attributes->set('_api_operation', $operation); + $dup->attributes->set('_api_operation_name', $operation->getName()); + $dup->attributes->remove('exception'); + // These are for swagger + $dup->attributes->set('_api_original_route', $request->attributes->get('_route')); + $dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables')); + $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params')); + $dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation')); + + foreach ($identifiers as $name => $value) { + $dup->attributes->set($name, $value); + } + + return $this->apiPlatformController->__invoke($dup); + }); + } + + private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $errorOperation, \Throwable $exception): int + { + $exceptionToStatus = array_merge( + $apiOperation ? $apiOperation->getExceptionToStatus() ?? [] : [], + $errorOperation ? $errorOperation->getExceptionToStatus() ?? [] : [], + $this->exceptionToStatus ?? [] + ); + + foreach ($exceptionToStatus as $class => $status) { + if (is_a($exception::class, $class, true)) { + return $status; + } + } + + if ($exception instanceof AuthenticationException) { + return 401; + } + + if ($exception instanceof AuthorizationException) { + return 403; + } + + if ($exception instanceof SymfonyHttpExceptionInterface) { + return $exception->getStatusCode(); + } + + if ($exception instanceof SymfonyHttpExceptionInterface) { + return $exception->getStatusCode(); + } + + if ($exception instanceof RequestExceptionInterface) { + return 400; + } + + // if ($exception instanceof ValidationException) { + // return 422; + // } + + if ($status = $errorOperation?->getStatus()) { + return $status; + } + + return 500; + } + + private function getFormatOperation(?string $format): string + { + return match ($format) { + 'json' => '_api_errors_problem', + 'jsonproblem' => '_api_errors_problem', + 'jsonld' => '_api_errors_hydra', + 'jsonapi' => '_api_errors_jsonapi', + 'html' => '_api_errors_problem', // This will be intercepted by the SwaggerUiProvider + default => '_api_errors_problem', + }; + } + + public static function provide(): mixed + { + if ($data = static::$error) { + return $data; + } + + throw new \LogicException(\sprintf('We could not find the thrown exception in the %s.', self::class)); + } +} diff --git a/src/Laravel/GraphQl/Controller/EntrypointController.php b/src/Laravel/GraphQl/Controller/EntrypointController.php new file mode 100644 index 00000000000..9d47eb514e4 --- /dev/null +++ b/src/Laravel/GraphQl/Controller/EntrypointController.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\GraphQl\Controller; + +use ApiPlatform\GraphQl\Error\ErrorHandlerInterface; +use ApiPlatform\GraphQl\ExecutorInterface; +use ApiPlatform\GraphQl\Type\SchemaBuilderInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use GraphQL\Error\DebugFlag; +use GraphQL\Error\Error; +use GraphQL\Executor\ExecutionResult; +use Illuminate\Http\Request; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class EntrypointController +{ + use ContentNegotiationTrait; + private int $debug; + + /** + * @param array $formats + */ + public function __construct( + private readonly SchemaBuilderInterface $schemaBuilder, + private readonly ExecutorInterface $executor, + private readonly GraphiQlController $graphiQlAction, + private readonly NormalizerInterface $normalizer, + private readonly ErrorHandlerInterface $errorHandler, + bool $debug = false, + ?Negotiator $negotiator = null, + private readonly array $formats = [], + ) { + $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; + $this->negotiator = $negotiator ?? new Negotiator(); + } + + public function __invoke(Request $request): Response + { + $formats = ['json' => ['application/json'], 'html' => ['text/html']]; + + foreach ($this->formats as $k => $f) { + if (!isset($formats[$k])) { + $formats[$k] = $f; + } + } + + $this->addRequestFormats($request, $formats); + $format = $this->getRequestFormat($request, $formats, false); + $request->setRequestFormat($format); + + try { + if ($request->isMethod('GET') && 'html' === $format) { + return ($this->graphiQlAction)(); + } + + [$query, $operationName, $variables] = $this->parseRequest($request, $format); + if (null === $query) { + throw new BadRequestHttpException('GraphQL query is not valid.'); + } + + $executionResult = $this->executor + ->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operationName) + ->setErrorsHandler($this->errorHandler) + ->setErrorFormatter($this->normalizer->normalize(...)); + } catch (\Exception $exception) { + $executionResult = (new ExecutionResult(null, [new Error($exception->getMessage(), null, null, [], null, $exception)])) + ->setErrorsHandler($this->errorHandler) + ->setErrorFormatter($this->normalizer->normalize(...)); + } + + return new JsonResponse($executionResult->toArray($this->debug)); + } + + /** + * @throws BadRequestHttpException + * + * @return array{0: array|null, 1: string, 2: array} + */ + private function parseRequest(Request $request, string $format): array + { + $queryParameters = $request->query->all(); + $query = $queryParameters['query'] ?? null; + $operationName = $queryParameters['operationName'] ?? null; + if ($variables = $queryParameters['variables'] ?? []) { + $variables = $this->decodeVariables($variables); + } + + if (!$request->isMethod('POST')) { + return [$query, $operationName, $variables]; + } + + if ('json' === $format) { + return $this->parseData($query, $operationName, $variables, $request->getContent()); + } + + if ('graphql' === $format) { + $query = $request->getContent(); + } + + if ('multipart' === $format) { + return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all()); + } + + return [$query, $operationName, $variables]; + } + + /** + * @param array $variables + * + * @throws BadRequestHttpException + * + * @return array{0: array, 1: string, 2: array} + */ + private function parseData(?string $query, ?string $operationName, array $variables, string $jsonContent): array + { + if (!\is_array($data = json_decode($jsonContent, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL data is not valid JSON.'); + } + + if (isset($data['query'])) { + $query = $data['query']; + } + + if (isset($data['variables'])) { + $variables = \is_array($data['variables']) ? $data['variables'] : $this->decodeVariables($data['variables']); + } + + if (isset($data['operationName'])) { + $operationName = $data['operationName']; + } + + return [$query, $operationName, $variables]; + } + + /** + * @param array $variables + * @param array $bodyParameters + * @param array $files + * + * @throws BadRequestHttpException + * + * @return array{0: array, 1: string, 2: array} + */ + private function parseMultipartRequest(?string $query, ?string $operationName, array $variables, array $bodyParameters, array $files): array + { + if ((null === $operations = $bodyParameters['operations'] ?? null) || (null === $map = $bodyParameters['map'] ?? null)) { + throw new BadRequestHttpException('GraphQL multipart request does not respect the specification.'); + } + + [$query, $operationName, $variables] = $this->parseData($query, $operationName, $variables, $operations); + + /** @var string $map */ + if (!\is_array($decodedMap = json_decode($map, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL multipart request map is not valid JSON.'); + } + + $variables = $this->applyMapToVariables($decodedMap, $variables, $files); + + return [$query, $operationName, $variables]; + } + + /** + * @param array $map + * @param array $variables + * @param array $files + * + * @throws BadRequestHttpException + */ + private function applyMapToVariables(array $map, array $variables, array $files): array + { + foreach ($map as $key => $value) { + if (null === $file = $files[$key] ?? null) { + throw new BadRequestHttpException('GraphQL multipart request file has not been sent correctly.'); + } + + foreach ($value as $mapValue) { + $path = explode('.', (string) $mapValue); + + if ('variables' !== $path[0]) { + throw new BadRequestHttpException('GraphQL multipart request path in map is invalid.'); + } + + unset($path[0]); + + $mapPathExistsInVariables = array_reduce($path, static fn (array $inVariables, string $pathElement) => \array_key_exists($pathElement, $inVariables) ? $inVariables[$pathElement] : false, $variables); + + if (false === $mapPathExistsInVariables) { + throw new BadRequestHttpException('GraphQL multipart request path in map does not match the variables.'); + } + + $variableFileValue = &$variables; + foreach ($path as $pathValue) { + $variableFileValue = &$variableFileValue[$pathValue]; + } + $variableFileValue = $file; + } + } + + return $variables; + } + + /** + * @throws BadRequestHttpException + * + * @return array + */ + private function decodeVariables(string $variables): array + { + if (!\is_array($decoded = json_decode($variables, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL variables are not valid JSON.'); + } + + return $decoded; + } +} diff --git a/src/Laravel/GraphQl/Controller/GraphiQlController.php b/src/Laravel/GraphQl/Controller/GraphiQlController.php new file mode 100644 index 00000000000..514d7a337e2 --- /dev/null +++ b/src/Laravel/GraphQl/Controller/GraphiQlController.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\GraphQl\Controller; + +use Illuminate\Http\Response; + +readonly class GraphiQlController +{ + public function __construct(private readonly string $prefix) + { + } + + public function __invoke(): Response + { + return new Response(view('api-platform::graphiql', ['graphiql_data' => ['entrypoint' => $this->prefix.'/graphql']]), 200); + } +} diff --git a/src/ParameterValidator/LICENSE b/src/Laravel/LICENSE similarity index 96% rename from src/ParameterValidator/LICENSE rename to src/Laravel/LICENSE index 1ca98eeb824..b228190b358 100644 --- a/src/ParameterValidator/LICENSE +++ b/src/Laravel/LICENSE @@ -1,6 +1,6 @@ The MIT license -Copyright (c) 2015-present Kévin Dunglas +Copyright (c) 2024-present Kévin Dunglas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Laravel/Metadata/CachePropertyMetadataFactory.php b/src/Laravel/Metadata/CachePropertyMetadataFactory.php new file mode 100644 index 00000000000..f3132a55d30 --- /dev/null +++ b/src/Laravel/Metadata/CachePropertyMetadataFactory.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Support\Facades\Cache; + +final readonly class CachePropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private PropertyMetadataFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + $key = hash('xxh3', serialize(['resource_class' => $resourceClass, 'property' => $property] + $options)); + + return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $property, $options) { + return $this->decorated->create($resourceClass, $property, $options); + }); + } +} diff --git a/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php b/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php new file mode 100644 index 00000000000..6269dd2080f --- /dev/null +++ b/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use Illuminate\Support\Facades\Cache; + +final readonly class CachePropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct( + private PropertyNameCollectionFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + $key = hash('xxh3', serialize(['resource_class' => $resourceClass] + $options)); + + return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $options) { + return $this->decorated->create($resourceClass, $options); + }); + } +} diff --git a/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php b/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php new file mode 100644 index 00000000000..83836d9aae4 --- /dev/null +++ b/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Support\Facades\Cache; + +final readonly class CacheResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private ResourceMetadataCollectionFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + return Cache::store($this->cacheStore)->rememberForever($resourceClass, function () use ($resourceClass) { + return $this->decorated->create($resourceClass); + }); + } +} diff --git a/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php b/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..a92fd20535d --- /dev/null +++ b/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Validation\Rule; +use Psr\Container\ContainerInterface; + +final class ParameterValidationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + private readonly ?ContainerInterface $filterLocator = null, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $i => $resource) { + $operations = $resource->getOperations(); + + foreach ($operations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($parameters as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + if (\count($parameters) > 0) { + $operations->add($operationName, $operation->withParameters($parameters)); + } + } + + $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort()); + + if (!$graphQlOperations = $resource->getGraphQlOperations()) { + continue; + } + + foreach ($graphQlOperations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($operation->getParameters() ?? [] as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + if (\count($parameters) > 0) { + $graphQlOperations[$operationName] = $operation->withParameters($parameters); + } + } + + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + } + + return $resourceMetadataCollection; + } + + private function addSchemaValidation(Parameter $parameter): Parameter + { + $schema = $parameter->getSchema(); + $required = $parameter->getRequired(); + $openApi = $parameter->getOpenApi(); + + // When it's an array of openapi parameters take the first one as it's probably just a variant of the query parameter, + // only getAllowEmptyValue is used here anyways + if (\is_array($openApi)) { + $openApi = $openApi[0]; + } + $assertions = []; + $allowEmptyValue = $openApi?->getAllowEmptyValue(); + if ($required || (false === $required && false === $allowEmptyValue)) { + $assertions[] = 'required'; + } + + if (true === $allowEmptyValue) { + $assertions[] = 'nullable'; + } + + if (isset($schema['exclusiveMinimum'])) { + $assertions[] = 'gt:'.$schema['exclusiveMinimum']; + } + + if (isset($schema['exclusiveMaximum'])) { + $assertions[] = 'lt:'.$schema['exclusiveMaximum']; + } + + if (isset($schema['minimum'])) { + $assertions[] = 'gte:'.$schema['minimum']; + } + + if (isset($schema['maximum'])) { + $assertions[] = 'lte:'.$schema['maximum']; + } + + if (isset($schema['pattern'])) { + $assertions[] = 'regex:'.$schema['pattern']; + } + + $minLength = isset($schema['minLength']); + $maxLength = isset($schema['maxLength']); + + if ($minLength && $maxLength) { + $assertions[] = \sprintf('between:%s,%s', $schema['minLength'], $schema['maxLength']); + } elseif ($minLength) { + $assertions[] = 'min:'.$schema['minLength']; + } elseif ($maxLength) { + $assertions[] = 'max:'.$schema['maxLength']; + } + + $minItems = isset($schema['minItems']); + $maxItems = isset($schema['maxItems']); + + if ($minItems && $maxItems) { + $assertions[] = \sprintf('between:%s,%s', $schema['minItems'], $schema['maxItems']); + } elseif ($minItems) { + $assertions[] = 'min:'.$schema['minItems']; + } elseif ($maxItems) { + $assertions[] = 'max:'.$schema['maxItems']; + } + + if (isset($schema['multipleOf'])) { + $assertions[] = 'multiple_of:'.$schema['multipleOf']; + } + + if (isset($schema['enum'])) { + $assertions[] = Rule::in($schema['enum']); + } + + if (isset($schema['type']) && 'array' === $schema['type']) { + $assertions[] = 'array'; + } + + if (!$assertions) { + return $parameter; + } + + if (1 === \count($assertions)) { + return $parameter->withConstraints($assertions[0]); + } + + return $parameter->withConstraints($assertions); + } +} diff --git a/src/Laravel/README.md b/src/Laravel/README.md new file mode 100644 index 00000000000..dd8b2d53c6f --- /dev/null +++ b/src/Laravel/README.md @@ -0,0 +1,12 @@ +# API Platform for Laravel + +Integration of [Laravel](https://laravel.com) and the Illuminate components with the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/laravel/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Laravel/Routing/IriConverter.php b/src/Laravel/Routing/IriConverter.php new file mode 100644 index 00000000000..cc9ab93a49a --- /dev/null +++ b/src/Laravel/Routing/IriConverter.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Database\Eloquent\Relations\Relation; +// use Illuminate\Routing\Router; +use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; +use Symfony\Component\Routing\RouterInterface; + +class IriConverter implements IriConverterInterface +{ + use ClassInfoTrait; + use ResourceClassInfoTrait; + // use UriVariablesResolverTrait; + + /** + * @var array + */ + private array $localOperationCache = []; + + /** + * @var array + */ + private array $localIdentifiersExtractorOperationCache = []; + + // , UriVariablesConverterInterface $uriVariablesConverter = null TODO + /** + * @param ProviderInterface $provider + */ + public function __construct(private readonly ProviderInterface $provider, private readonly OperationMetadataFactoryInterface $operationMetadataFactory, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?IriConverterInterface $decorated = null) + { + $this->resourceClassResolver = $resourceClassResolver; + } + + /** + * @param array $context + */ + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object + { + $parameters = $this->router->match($iri); + if (!isset($parameters['_api_resource_class'], $parameters['_api_operation_name'], $parameters['uri_variables'])) { + throw new InvalidArgumentException(\sprintf('No resource associated to "%s".', $iri)); + } + + $operation = $this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class'])->getOperation($parameters['_api_operation_name']); + + if ($operation instanceof CollectionOperationInterface) { + throw new InvalidArgumentException(\sprintf('The iri "%s" references a collection not an item.', $iri)); + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(\sprintf('The iri "%s" does not reference an HTTP operation.', $iri)); + } + + if ($item = $this->provider->provide($operation, $parameters['uri_variables'], $context)) { + return $item; // @phpstan-ignore-line + } + + throw new ItemNotFoundException(\sprintf('Item not found for "%s".', $iri)); + } + + /** + * @param array $context + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string + { + $resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource)); + if ($resource instanceof Relation) { + $resourceClass = $this->getObjectClass($resource->getRelated()); + } + + if (isset($context['item_uri_template'])) { + $operation = $this->operationMetadataFactory->create($context['item_uri_template']); + } + + $localOperationCacheKey = ($operation?->getName() ?? '').$resourceClass.(\is_string($resource) ? '_c' : '_i'); + if ($operation && isset($this->localOperationCache[$localOperationCacheKey])) { + return $this->generateRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context, $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? null); + } + + if (!$this->resourceClassResolver->isResourceClass($resourceClass)) { + return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass); + } + + // This is only for when a class (that is not a resource) extends another one that is a resource, we should remove this behavior + if (!\is_string($resource) && !isset($context['force_resource_class'])) { + $resourceClass = $this->getResourceClass($resource, true); + } + + if (!$operation) { + $operation = (new Get())->withClass($resourceClass); + } + + if ($operation instanceof HttpOperation && 301 === $operation->getStatus()) { + $operation = ($operation instanceof CollectionOperationInterface ? new GetCollection() : new Get())->withClass($operation->getClass()); + unset($context['uri_variables']); + } + + $identifiersExtractorOperation = $operation; + // In symfony the operation name is the route name, try to find one if none provided + if ( + !$operation->getName() + || ($operation instanceof HttpOperation && 'POST' === $operation->getMethod()) + ) { + $forceCollection = $operation instanceof CollectionOperationInterface; + try { + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true); + $identifiersExtractorOperation = $operation; + } catch (OperationNotFoundException) { + } + } + + if (!$operation->getName() || ($operation instanceof HttpOperation && $operation->getUriTemplate() && str_starts_with($operation->getUriTemplate(), SkolemIriConverter::SKOLEM_URI_TEMPLATE))) { + return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass); + } + + $this->localOperationCache[$localOperationCacheKey] = $operation; + $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] = $identifiersExtractorOperation; + + return $this->generateRoute($resource, $referenceType, $operation, $context, $identifiersExtractorOperation); + } + + /** + * @param array $context + */ + private function generateRoute(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?Operation $identifiersExtractorOperation = null): string + { + $identifiers = $context['uri_variables'] ?? []; + + if (\is_object($resource)) { + try { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, $identifiersExtractorOperation, $context); + } catch (InvalidArgumentException|RuntimeException $e) { + // We can try using context uri variables if any + if (!$identifiers) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e); + } + } + } + + try { + return $this->router->generate($operation->getName(), $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType); + } catch (RoutingExceptionInterface $e) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e); + } + } + + /** + * @param object|class-string $resource + * @param array $context + */ + private function generateSkolemIri(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?string $resourceClass = null): string + { + if (!$this->decorated) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass)); + } + + // Use a skolem iri, the route is defined in genid.xml + return $this->decorated->getIriFromResource($resource, $referenceType, $operation, $context); + } +} diff --git a/src/Laravel/Routing/Router.php b/src/Laravel/Routing/Router.php new file mode 100644 index 00000000000..6bb47fada8b --- /dev/null +++ b/src/Laravel/Routing/Router.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\UrlGeneratorInterface; +use Illuminate\Http\Request as LaravelRequest; +use Illuminate\Routing\Router as BaseRouter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RouterInterface; + +/** + * Laravel router decorator. + * + * @author Kévin Dunglas + */ +final class Router implements RouterInterface, UrlGeneratorInterface +{ + public const CONST_MAP = [ + UrlGeneratorInterface::ABS_URL => RouterInterface::ABSOLUTE_URL, + UrlGeneratorInterface::ABS_PATH => RouterInterface::ABSOLUTE_PATH, + UrlGeneratorInterface::REL_PATH => RouterInterface::RELATIVE_PATH, + UrlGeneratorInterface::NET_PATH => RouterInterface::NETWORK_PATH, + ]; + + private RequestContext $context; + + public function __construct(private readonly BaseRouter $router, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH) + { + } + + /** + * {@inheritdoc} + */ + public function setContext(RequestContext $context): void + { + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function getContext(): RequestContext + { + return $this->context; + } + + /** + * {@inheritdoc} + */ + public function getRouteCollection(): RouteCollection + { + /** @var \Illuminate\Routing\RouteCollection $routes */ + $routes = $this->router->getRoutes(); + + return $routes->toSymfonyRouteCollection(); + } + + /** + * {@inheritdoc} + * + * @return array|array{_api_resource_class?: class-string|string, _api_operation_name?: string, uri_variables?: array} + */ + public function match(string $pathInfo): array + { + $request = LaravelRequest::create($pathInfo, Request::METHOD_GET); + $route = $this->router->getRoutes()->match($request); + + return $route->defaults + ['uri_variables' => array_diff_key($route->parameters, $route->defaults)]; + } + + /** + * {@inheritdoc} + * + * @param array $parameters + */ + public function generate(string $name, array $parameters = [], ?int $referenceType = null): string + { + $routes = $this->getRouteCollection(); + $generator = new UrlGenerator($routes, $this->getContext()); + if (isset($parameters['_format']) && !str_starts_with($parameters['_format'], '.')) { + $parameters['_format'] = '.'.$parameters['_format']; + } + + return $generator->generate($name, $parameters, self::CONST_MAP[$referenceType ?? $this->urlGenerationStrategy]); + } +} diff --git a/src/Laravel/Routing/SkolemIriConverter.php b/src/Laravel/Routing/SkolemIriConverter.php new file mode 100644 index 00000000000..6aa7f16f6c2 --- /dev/null +++ b/src/Laravel/Routing/SkolemIriConverter.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\UrlGeneratorInterface; + +/** + * {@inheritdoc} + * + * @author Antoine Bluchet + */ +final class SkolemIriConverter implements IriConverterInterface +{ + public const SKOLEM_URI_TEMPLATE = '/.well-known/genid/{id}'; + + /** + * @var \SplObjectStorage + */ + private \SplObjectStorage $objectHashMap; + + /** + * @var array + */ + private array $classHashMap = []; + + public function __construct(private readonly Router $router) + { + $this->objectHashMap = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object + { + throw new ItemNotFoundException(\sprintf('Item not found for "%s".', $iri)); + } + + /** + * {@inheritdoc} + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string + { + $referenceType = $operation ? ($operation->getUrlGenerationStrategy() ?? $referenceType) : $referenceType; + if (($isObject = \is_object($resource)) && $this->objectHashMap->contains($resource)) { + return $this->router->generate('api_genid', ['id' => $this->objectHashMap[$resource]], $referenceType); + } + + if (\is_string($resource) && isset($this->classHashMap[$resource])) { + return $this->router->generate('api_genid', ['id' => $this->classHashMap[$resource]], $referenceType); + } + + $id = bin2hex(random_bytes(10)); + + if ($isObject) { + $this->objectHashMap[$resource] = $id; + } else { + $this->classHashMap[$resource] = $id; + } + + return $this->router->generate('api_genid', ['id' => $id], $referenceType); + } +} diff --git a/src/Laravel/Security/ResourceAccessChecker.php b/src/Laravel/Security/ResourceAccessChecker.php new file mode 100644 index 00000000000..5633ea1c808 --- /dev/null +++ b/src/Laravel/Security/ResourceAccessChecker.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Security; + +use ApiPlatform\Laravel\Eloquent\Paginator; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use Illuminate\Support\Facades\Gate; + +class ResourceAccessChecker implements ResourceAccessCheckerInterface +{ + public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool + { + return Gate::allows( + $expression, + $extraVariables['object'] instanceof Paginator ? + $resourceClass : + $extraVariables['object'] + ); + } +} diff --git a/src/Laravel/ServiceLocator.php b/src/Laravel/ServiceLocator.php new file mode 100644 index 00000000000..c774f8369d0 --- /dev/null +++ b/src/Laravel/ServiceLocator.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use Psr\Container\ContainerInterface; + +// TODO: template T ServiceLocator +final class ServiceLocator implements ContainerInterface +{ + private array $services = []; + + /** + * @param array $services + */ + public function __construct(array $services = []) + { + foreach ($services as $key => $service) { + $this->services[\is_string($key) ? $key : $service::class] = $service; + } + } + + public function get(string $id): mixed + { + return $this->services[$id] ?? null; + } + + public function has(string $id): bool + { + return isset($this->services[$id]); + } +} diff --git a/src/Laravel/State/AccessCheckerProvider.php b/src/Laravel/State/AccessCheckerProvider.php new file mode 100644 index 00000000000..6e82e68f4ea --- /dev/null +++ b/src/Laravel/State/AccessCheckerProvider.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Auth\Access\AuthorizationException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + +/** + * Allows access based on the ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface. + * This implementation covers GraphQl and HTTP. + * + * @see ResourceAccessCheckerInterface + * + * @implements ProviderInterface + */ +final class AccessCheckerProvider implements ProviderInterface +{ + /** + * @param ProviderInterface $decorated + */ + public function __construct(private readonly ProviderInterface $decorated, private readonly ResourceAccessCheckerInterface $resourceAccessChecker) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $policy = $operation->getPolicy(); + $message = $operation->getSecurityMessage(); + + $body = $this->decorated->provide($operation, $uriVariables, $context); + if (null === $policy) { + return $body; + } + + $request = $context['request'] ?? null; + + $resourceAccessCheckerContext = [ + 'object' => $body, + 'request' => $request, + 'operation' => $operation, + ]; + + if (!$this->resourceAccessChecker->isGranted($operation->getClass(), $policy, $resourceAccessCheckerContext)) { + throw $operation instanceof HttpOperation ? new AuthorizationException($message ?? 'Access Denied.') : new AccessDeniedHttpException($message ?? 'Access Denied.'); + } + + return $body; + } +} diff --git a/src/Laravel/State/ParameterValidatorProvider.php b/src/Laravel/State/ParameterValidatorProvider.php new file mode 100644 index 00000000000..d3f4fab3d01 --- /dev/null +++ b/src/Laravel/State/ParameterValidatorProvider.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ParameterNotFound; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\ParameterParserTrait; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Request; + +/** + * Validates parameters using the Symfony validator. + * + * @implements ProviderInterface + * + * @experimental + */ +final class ParameterValidatorProvider implements ProviderInterface +{ + use ParameterParserTrait; + use ValidationErrorTrait; + + /** + * @param ProviderInterface $decorated + */ + public function __construct( + private readonly ?ProviderInterface $decorated = null, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if (!($request = $context['request'] ?? null) instanceof Request) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + $operation = $request->attributes->get('_api_operation') ?? $operation; + if (!($operation->getQueryParameterValidationEnabled() ?? true)) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + $allConstraints = []; + foreach ($operation->getParameters() ?? [] as $parameter) { + if (!$constraints = $parameter->getConstraints()) { + continue; + } + + $key = $parameter->getKey(); + if (null === $key) { + throw new RuntimeException('A parameter must have a defined key.'); + } + + $value = $parameter->getValue(); + if ($value instanceof ParameterNotFound) { + $value = null; + } + + // Basically renames our key from order[:property] to order.* to assign the rule properly (see https://laravel.com/docs/11.x/validation#rule-in) + if (str_contains($key, '[:property]')) { + $k = str_replace('[:property]', '', $key); + $allConstraints[$k.'.*'] = $constraints; + continue; + } + + $allConstraints[$key] = $constraints; + } + + $validator = Validator::make($request->query->all(), $allConstraints); + if ($validator->fails()) { + throw $this->getValidationError($validator, new ValidationException($validator)); + } + + return $this->decorated->provide($operation, $uriVariables, $context); + } +} diff --git a/src/Laravel/State/SwaggerUiProcessor.php b/src/Laravel/State/SwaggerUiProcessor.php new file mode 100644 index 00000000000..6a90a23fdc9 --- /dev/null +++ b/src/Laravel/State/SwaggerUiProcessor.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\OpenApi\Options; +use ApiPlatform\OpenApi\Serializer\NormalizeOperationNameTrait; +use ApiPlatform\State\ProcessorInterface; +use Illuminate\Http\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @internal + * + * @implements ProcessorInterface + */ +final class SwaggerUiProcessor implements ProcessorInterface +{ + use NormalizeOperationNameTrait; + + /** + * @param array $formats + */ + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly NormalizerInterface $normalizer, + private readonly Options $openApiOptions, + private readonly array $formats = [], + private readonly ?string $oauthClientId = null, + private readonly ?string $oauthClientSecret = null, + private readonly bool $oauthPkce = false, + ) { + } + + /** + * @param OpenApi $openApi + */ + public function process(mixed $openApi, Operation $operation, array $uriVariables = [], array $context = []): Response + { + $request = $context['request'] ?? null; + + $swaggerContext = [ + 'formats' => $this->formats, + 'title' => $openApi->getInfo()->getTitle(), + 'description' => $openApi->getInfo()->getDescription(), + 'originalRoute' => $request->attributes->get('_api_original_route', $request->attributes->get('_route')), + 'originalRouteParams' => $request->attributes->get('_api_original_route_params', $request->attributes->get('_route_params', [])), + ]; + + $swaggerData = [ + 'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']), + 'spec' => $this->normalizer->normalize($openApi, 'json', []), + 'oauth' => [ + 'enabled' => $this->openApiOptions->getOAuthEnabled(), + 'type' => $this->openApiOptions->getOAuthType(), + 'flow' => $this->openApiOptions->getOAuthFlow(), + 'tokenUrl' => $this->openApiOptions->getOAuthTokenUrl(), + 'authorizationUrl' => $this->openApiOptions->getOAuthAuthorizationUrl(), + 'redirectUrl' => $request->getSchemeAndHttpHost().'/vendor/api-platform/swagger-ui/oauth2-redirect.html', + 'scopes' => $this->openApiOptions->getOAuthScopes(), + 'clientId' => $this->oauthClientId, + 'clientSecret' => $this->oauthClientSecret, + 'pkce' => $this->oauthPkce, + ], + ]; + + $status = 200; + $requestedOperation = $request?->attributes->get('_api_requested_operation') ?? null; + if ($request->isMethodSafe() && $requestedOperation && $requestedOperation->getName()) { + // TODO: what if the parameter is named something else then `id`? + $swaggerData['id'] = ($request->attributes->get('_api_original_uri_variables') ?? [])['id'] ?? null; + $swaggerData['queryParameters'] = $request->query->all(); + + $swaggerData['shortName'] = $requestedOperation->getShortName(); + $swaggerData['operationId'] = $this->normalizeOperationName($requestedOperation->getName()); + + [$swaggerData['path'], $swaggerData['method']] = $this->getPathAndMethod($swaggerData); + $status = $requestedOperation->getStatus() ?? $status; + } + + return new Response(view('api-platform::swagger-ui', $swaggerContext + ['swagger_data' => $swaggerData]), 200); + } + + /** + * @param array $swaggerData + * + * @return array{0: string, 1: string} + */ + private function getPathAndMethod(array $swaggerData): array + { + foreach ($swaggerData['spec']['paths'] as $path => $operations) { + foreach ($operations as $method => $operation) { + if (($operation['operationId'] ?? null) === $swaggerData['operationId']) { + return [$path, $method]; + } + } + } + + throw new RuntimeException(\sprintf('The operation "%s" cannot be found in the Swagger specification.', $swaggerData['operationId'])); + } +} diff --git a/src/Laravel/State/SwaggerUiProvider.php b/src/Laravel/State/SwaggerUiProvider.php new file mode 100644 index 00000000000..e951fe2e00c --- /dev/null +++ b/src/Laravel/State/SwaggerUiProvider.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\State\ProviderInterface; + +/** + * When an HTML request is sent we provide a swagger ui documentation. + * + * @implements ProviderInterface + * + * @internal + */ +final class SwaggerUiProvider implements ProviderInterface +{ + /** + * @param ProviderInterface $decorated + */ + public function __construct( + private readonly ProviderInterface $decorated, + private readonly OpenApiFactoryInterface $openApiFactory, + private readonly bool $swaggerUiEnabled = true, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + // We went through the DocumentationAction + if (OpenApi::class === $operation->getClass()) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + if ( + !($operation instanceof HttpOperation) + || !($request = $context['request'] ?? null) + || 'html' !== $request->getRequestFormat() + || !$this->swaggerUiEnabled + ) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + if (!$request->attributes->has('_api_requested_operation')) { + $request->attributes->set('_api_requested_operation', $operation); + } + + // We need to call our operation provider just in case it fails + // when it fails we'll get an Error, and we'll fix the status accordingly + // @see features/main/content_negotiation.feature:119 + // DocumentationAction has no content negotiation as well we want HTML so render swagger ui + if (!$operation instanceof Error && Documentation::class !== $operation->getClass()) { + $this->decorated->provide($operation, $uriVariables, $context); + } + + $swaggerUiOperation = new Get( + class: OpenApi::class, + processor: 'api_platform.swagger_ui.processor', + validate: false, + read: false, + write: true, // force write so that our processor gets called + status: $operation->getStatus() + ); + + // save our operation + $request->attributes->set('_api_operation', $swaggerUiOperation); + + $data = $this->openApiFactory->__invoke(['base_url' => $request->getBaseUrl() ?: '/']); + $request->attributes->set('data', $data); + + return $data; + } +} diff --git a/src/Laravel/State/ValidateProvider.php b/src/Laravel/State/ValidateProvider.php new file mode 100644 index 00000000000..5e065ffd1b4 --- /dev/null +++ b/src/Laravel/State/ValidateProvider.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; + +/** + * @implements ProviderInterface + */ +final class ValidateProvider implements ProviderInterface +{ + use ValidationErrorTrait; + + /** + * @param ProviderInterface $inner + */ + public function __construct( + private readonly ProviderInterface $inner, + private readonly Application $app, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $request = $context['request']; + $body = $this->inner->provide($operation, $uriVariables, $context); + + if ($operation instanceof Error) { + return $body; + } + + $rules = $operation->getRules(); + if (\is_callable($rules)) { + $rules = $rules(); + } + + if (\is_string($rules) && is_a($rules, FormRequest::class, true)) { + try { + // this also throws an AuthorizationException + $this->app->make($rules); + } catch (ValidationException $e) { // @phpstan-ignore-line make->($rules) may throw this + if (!$operation->canValidate()) { + return $body; + } + + throw $this->getValidationError($e->validator, $e); + } + + return $body; + } + + if (!$operation->canValidate()) { + return $body; + } + + if (!\is_array($rules)) { + return $body; + } + + // In Symfony, validation is done on the Resource object (here $body) using Deserialization before Validation + // Here, we did not deserialize yet, we validate on the raw body before. + $validationBody = $request->request->all(); + if ('jsonapi' === $request->getRequestFormat()) { + $validationBody = $validationBody['data']['attributes']; + } + + $validator = Validator::make($validationBody, $rules); + if ($validator->fails()) { + throw $this->getValidationError($validator, new ValidationException($validator)); + } + + return $body; + } +} diff --git a/src/Laravel/State/ValidationErrorTrait.php b/src/Laravel/State/ValidationErrorTrait.php new file mode 100644 index 00000000000..3d2b3ade662 --- /dev/null +++ b/src/Laravel/State/ValidationErrorTrait.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Laravel\ApiResource\ValidationError; +use Illuminate\Contracts\Validation\Validator; +use Illuminate\Validation\ValidationException; + +trait ValidationErrorTrait +{ + private function getValidationError(Validator $validator, ValidationException $e): ValidationError + { + $errors = $validator->errors(); + $violations = []; + $id = hash('xxh3', implode(',', $errors->keys())); + foreach ($errors->messages() as $prop => $message) { + $violations[] = ['propertyPath' => $prop, 'message' => implode(\PHP_EOL, $message)]; + } + + return new ValidationError($e->getMessage(), $id, $e, $violations); + } +} diff --git a/src/Laravel/Test/ApiTestAssertionsTrait.php b/src/Laravel/Test/ApiTestAssertionsTrait.php new file mode 100644 index 00000000000..9752fe134a1 --- /dev/null +++ b/src/Laravel/Test/ApiTestAssertionsTrait.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test; + +use ApiPlatform\Laravel\Test\Constraint\ArraySubset; +use ApiPlatform\Metadata\IriConverterInterface; +use PHPUnit\Framework\ExpectationFailedException; + +trait ApiTestAssertionsTrait +{ + /** + * Asserts that an array has a specified subset. + * + * Imported from dms/phpunit-arraysubset, because the original constraint has been deprecated. + * + * @copyright Sebastian Bergmann + * @copyright Rafael Dohms + * + * @see https://github.com/sebastianbergmann/phpunit/issues/3494 + * + * @param array $subset + * @param array $array + * + * @throws ExpectationFailedException + * @throws \Exception + */ + public static function assertArraySubset(iterable $subset, iterable $array, bool $checkForObjectIdentity = false, string $message = ''): void + { + $constraint = new ArraySubset($subset, $checkForObjectIdentity); + + static::assertThat($array, $constraint, $message); + } + + /** + * Asserts that the retrieved JSON contains the specified subset. + * + * This method delegates to static::assertArraySubset(). + * + * @param array $subset + * @param array $json + */ + public static function assertJsonContains(array|string $subset, array $json, bool $checkForObjectIdentity = true, string $message = ''): void + { + if (\is_string($subset)) { + $subset = json_decode($subset, true, 512, \JSON_THROW_ON_ERROR); + } + if (!\is_array($subset)) { + throw new \InvalidArgumentException('$subset must be array or string (JSON array or JSON object)'); + } + + static::assertArraySubset($subset, $json, $checkForObjectIdentity, $message); + } + + /** + * Generates the IRI of a resource item. + */ + protected function getIriFromResource(object $resource): ?string + { + $iriConverter = $this->app->make(IriConverterInterface::class); + + return $iriConverter->getIriFromResource($resource); + } +} diff --git a/src/Laravel/Test/Constraint/ArraySubset.php b/src/Laravel/Test/Constraint/ArraySubset.php new file mode 100644 index 00000000000..9727721c70b --- /dev/null +++ b/src/Laravel/Test/Constraint/ArraySubset.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; + +/** + * Is used for phpunit >= 9. + * + * @internal + */ +final class ArraySubset extends Constraint +{ + use ArraySubsetTrait; + + /** + * {@inheritdoc} + */ + public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool + { + return $this->_evaluate($other, $description, $returnResult); + } +} diff --git a/src/Laravel/Test/Constraint/ArraySubsetTrait.php b/src/Laravel/Test/Constraint/ArraySubsetTrait.php new file mode 100644 index 00000000000..692b6a2ada4 --- /dev/null +++ b/src/Laravel/Test/Constraint/ArraySubsetTrait.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test\Constraint; + +use SebastianBergmann\Comparator\ComparisonFailure; +use SebastianBergmann\Exporter\Exporter; + +/** + * Constraint that asserts that the array it is evaluated for has a specified subset. + * + * Uses array_replace_recursive() to check if a key value subset is part of the + * subject array. + * + * Imported from dms/phpunit-arraysubset-asserts, because the original constraint has been deprecated. + * + * @copyright Sebastian Bergmann + * @copyright Rafael Dohms + * + * @see https://github.com/sebastianbergmann/phpunit/issues/3494 + */ +trait ArraySubsetTrait +{ + /** + * @param array $subset + */ + public function __construct(private iterable $subset, private readonly bool $strict = false) + { + } + + private function _evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool + { + // type cast $other & $this->subset as an array to allow + // support in standard array functions. + $other = $this->toArray($other); + $this->subset = $this->toArray($this->subset); + $patched = array_replace_recursive($other, $this->subset); + if ($this->strict) { + $result = $other === $patched; + } else { + $result = $other == $patched; + } + if ($returnResult) { + return $result; + } + if ($result) { + return null; + } + + $f = new ComparisonFailure( + $patched, + $other, + var_export($patched, true), + var_export($other, true) + ); + $this->fail($other, $description, $f); + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'has the subset '.(new Exporter())->export($this->subset); + } + + /** + * {@inheritdoc} + */ + protected function failureDescription(mixed $other): string + { + return 'an array '.$this->toString(); + } + + /** + * @param array $other + * + * @return array + */ + private function toArray(iterable $other): array + { + if (\is_array($other)) { + return $other; + } + if ($other instanceof \ArrayObject) { + return $other->getArrayCopy(); + } + + return iterator_to_array($other); + } +} diff --git a/src/Laravel/Tests/AuthTest.php b/src/Laravel/Tests/AuthTest.php new file mode 100644 index 00000000000..4b7d3bc8c9b --- /dev/null +++ b/src/Laravel/Tests/AuthTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\UserFactory; + +class AuthTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.graphql.enabled', true); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + }); + } + + protected function afterRefreshingDatabase(): void + { + UserFactory::new()->create(); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/vaults', ['accept' => ['application/ld+json']]); + $this->assertArraySubset(['detail' => 'Unauthenticated.'], $response->json()); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertStatus(401); + } + + public function testAuthenticated(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->get('/api/vaults', ['accept' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + } + + public function testAuthenticatedPolicy(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/vaults', [], ['accept' => ['application/ld+json'], 'content-type' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(403); + } + + public function testAuthenticatedDeleteWithPolicy(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->delete('/api/vaults/1', [], ['accept' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(403); + } +} diff --git a/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php b/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php new file mode 100644 index 00000000000..c51b5de1fc9 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker; + +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\AppServiceFileGenerator; +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\PathResolver; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class MakeStateProcessorCommandTest extends TestCase +{ + use WithWorkbench; + + /** @var string */ + private const STATE_PROCESSOR_COMMAND = 'make:state-processor'; + /** @var string */ + private const CHOSEN_CLASS_NAME = 'Choose a class name for your state processor (e.g. AwesomeStateProcessor)'; + + private ?Filesystem $filesystem; + private PathResolver $pathResolver; + private AppServiceFileGenerator $appServiceFileGenerator; + + /** + * @throws FileNotFoundException + */ + protected function setup(): void + { + parent::setUp(); + + $this->filesystem = new Filesystem(); + $this->pathResolver = new PathResolver(); + $this->appServiceFileGenerator = new AppServiceFileGenerator($this->filesystem); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } + + /** + * @throws FileNotFoundException + */ + public function testMakeStateProviderCommand(): void + { + $processorName = 'MyStateProcessor'; + $filePath = $this->pathResolver->generateStateFilename($processorName); + $appServiceProviderPath = $this->pathResolver->getServiceProviderFilePath(); + + $this->artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $processorName) + ->expectsOutputToContain('Success!') + ->expectsOutputToContain("created: $filePath") + ->expectsOutputToContain('Next: Open your new state processor class and start customizing it.') + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $appServiceProviderContent = $this->filesystem->get($appServiceProviderPath); + $this->assertStringContainsString('use ApiPlatform\State\ProcessorInterface;', $appServiceProviderContent); + $this->assertStringContainsString("use App\State\\$processorName;", $appServiceProviderContent); + $this->assertStringContainsString('$this->app->tag(MyStateProcessor::class, ProcessorInterface::class);', $appServiceProviderContent); + + $this->filesystem->delete($filePath); + } + + public function testWhenStateProviderClassAlreadyExists(): void + { + $processorName = 'ExistingProcessor'; + $existingFile = $this->pathResolver->generateStateFilename($processorName); + $this->filesystem->put($existingFile, 'artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $processorName) + ->expectsOutput($expectedError) + ->assertExitCode(Command::FAILURE); + + $this->filesystem->delete($existingFile); + } + + public function testMakeStateProviderCommandWithoutGivenClassName(): void + { + $processorName = 'NoEmptyClassName'; + $filePath = $this->pathResolver->generateStateFilename($processorName); + + $this->artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, '') + ->expectsOutput('[ERROR] This value cannot be blank.') + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $processorName) + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $this->filesystem->delete($filePath); + } + + /** + * @throws FileNotFoundException + */ + protected function tearDown(): void + { + parent::tearDown(); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } +} diff --git a/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php b/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php new file mode 100644 index 00000000000..c259c79fa39 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker; + +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\AppServiceFileGenerator; +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\PathResolver; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class MakeStateProviderCommandTest extends TestCase +{ + use WithWorkbench; + + /** @var string */ + private const MAKE_STATE_PROVIDER_COMMAND = 'make:state-provider'; + /** @var string */ + private const STATE_PROVIDER_CLASS_NAME = 'Choose a class name for your state provider (e.g. AwesomeStateProvider)'; + + private ?Filesystem $filesystem; + private PathResolver $pathResolver; + private AppServiceFileGenerator $appServiceFileGenerator; + + /** + * @throws FileNotFoundException + */ + protected function setup(): void + { + parent::setUp(); + + $this->filesystem = new Filesystem(); + $this->pathResolver = new PathResolver(); + $this->appServiceFileGenerator = new AppServiceFileGenerator($this->filesystem); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } + + /** + * @throws FileNotFoundException + */ + public function testMakeStateProviderCommand(): void + { + $providerName = 'MyStateProvider'; + $filePath = $this->pathResolver->generateStateFilename($providerName); + $appServiceProviderPath = $this->pathResolver->getServiceProviderFilePath(); + + $this->artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $providerName) + ->expectsOutputToContain('Success!') + ->expectsOutputToContain("created: $filePath") + ->expectsOutputToContain('Next: Open your new state provider class and start customizing it.') + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $appServiceProviderContent = $this->filesystem->get($appServiceProviderPath); + $this->assertStringContainsString('use ApiPlatform\State\ProviderInterface;', $appServiceProviderContent); + $this->assertStringContainsString("use App\State\\$providerName;", $appServiceProviderContent); + $this->assertStringContainsString('$this->app->tag(MyStateProvider::class, ProviderInterface::class);', $appServiceProviderContent); + + $this->filesystem->delete($filePath); + } + + public function testWhenStateProviderClassAlreadyExists(): void + { + $providerName = 'ExistingProvider'; + $existingFile = $this->pathResolver->generateStateFilename($providerName); + $this->filesystem->put($existingFile, 'artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $providerName) + ->expectsOutput($expectedError) + ->assertExitCode(Command::FAILURE); + + $this->filesystem->delete($existingFile); + } + + public function testMakeStateProviderCommandWithoutGivenClassName(): void + { + $providerName = 'NoEmptyClassName'; + $filePath = $this->pathResolver->generateStateFilename($providerName); + + $this->artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, '') + ->expectsOutput('[ERROR] This value cannot be blank.') + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $providerName) + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $this->filesystem->delete($filePath); + } + + /** + * @throws FileNotFoundException + */ + protected function tearDown(): void + { + parent::tearDown(); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } +} diff --git a/src/Symfony/Bundle/Command/OpenApiCommand.php b/src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.tpl.php similarity index 57% rename from src/Symfony/Bundle/Command/OpenApiCommand.php rename to src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.tpl.php index 694591d17fd..ff02bb3a99d 100644 --- a/src/Symfony/Bundle/Command/OpenApiCommand.php +++ b/src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.tpl.php @@ -11,12 +11,17 @@ declare(strict_types=1); -namespace ApiPlatform\Symfony\Bundle\Command; +namespace App\Providers; -class_exists(\ApiPlatform\OpenApi\Command\OpenApiCommand::class); +use Illuminate\Support\ServiceProvider; -if (false) { - final class OpenApiCommand extends \ApiPlatform\OpenApi\Command\OpenApiCommand +class AppServiceProvider extends ServiceProvider +{ + public function boot(): void + { + } + + public function register(): void { } } diff --git a/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php b/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php new file mode 100644 index 00000000000..6edd7363b03 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class AppServiceFileGenerator +{ + public function __construct(private Filesystem $filesystem) + { + } + + /** + * @throws FileNotFoundException + */ + public function regenerateProviderFile(): void + { + $templatePath = \dirname(__DIR__).'/Resources/skeleton/AppServiceProvider.tpl.php'; + $targetPath = base_path('app/Providers/AppServiceProvider.php'); + + $this->regenerateFileFromTemplate($templatePath, $targetPath); + } + + /** + * @throws FileNotFoundException + */ + private function regenerateFileFromTemplate(string $templatePath, string $targetPath): void + { + $content = $this->filesystem->get($templatePath); + + $this->filesystem->put($targetPath, $content); + } +} diff --git a/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php b/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php new file mode 100644 index 00000000000..ba04cacfbfc --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker\Utils; + +final readonly class PathResolver +{ + public function getServiceProviderFilePath(): string + { + return base_path('app/Providers/AppServiceProvider.php'); + } + + public function generateStateFilename(string $stateFilename): string + { + return $this->getStateDirectoryPath().$stateFilename.'.php'; + } + + public function getStateDirectoryPath(): string + { + return base_path('app/State/'); + } +} diff --git a/src/Laravel/Tests/DocsTest.php b/src/Laravel/Tests/DocsTest.php new file mode 100644 index 00000000000..9d155f041d3 --- /dev/null +++ b/src/Laravel/Tests/DocsTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class DocsTest extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + public function testOpenApi(): void + { + $res = $this->get('/api/docs.jsonopenapi'); + $this->assertArrayHasKey('openapi', $res->json()); + $this->assertSame('application/vnd.openapi+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testOpenApiAccept(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'application/vnd.openapi+json']); + $this->assertArrayHasKey('openapi', $res->json()); + $this->assertSame('application/vnd.openapi+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testJsonLd(): void + { + $res = $this->get('/api/docs.jsonld'); + $this->assertArrayHasKey('@context', $res->json()); + $this->assertSame('application/ld+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testJsonLdAccept(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'application/ld+json']); + $this->assertArrayHasKey('@context', $res->json()); + $this->assertSame('application/ld+json; charset=utf-8', $res->headers->get('content-type')); + } +} diff --git a/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php b/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php new file mode 100644 index 00000000000..2cac345483a --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Filter; + +use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; +use ApiPlatform\Metadata\QueryParameter; +use Illuminate\Database\Eloquent\Builder; +use PHPUnit\Framework\TestCase; + +class DateFilterTest extends TestCase +{ + public function testOperator(): void + { + $f = new DateFilter(); + $builder = $this->createStub(Builder::class); + $this->assertEquals($builder, $f->apply($builder, ['neq' => '2020-02-02'], new QueryParameter(key: 'date', property: 'date'))); + } +} diff --git a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php new file mode 100644 index 00000000000..c76babf0703 --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; + +/** + * @author Tobias Oitzinger + */ +final class ModelMetadataTest extends TestCase +{ + use RefreshDatabase; + use WithWorkbench; + + public function testHiddenAttributesAreCorrectlyIdentified(): void + { + $model = new class extends Model { + protected $hidden = ['secret']; + + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(0, $metadata->getRelations($model)); + } + + public function testVisibleAttributesAreCorrectlyIdentified(): void + { + $model = new class extends Model { + protected $visible = ['secret']; + + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(1, $metadata->getRelations($model)); + } + + public function testAllAttributesVisibleByDefault(): void + { + $model = new class extends Model { + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(1, $metadata->getRelations($model)); + } +} diff --git a/src/Laravel/Tests/EloquentTest.php b/src/Laravel/Tests/EloquentTest.php new file mode 100644 index 00000000000..ad847eeced9 --- /dev/null +++ b/src/Laravel/Tests/EloquentTest.php @@ -0,0 +1,409 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class EloquentTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + public function testSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + $response = $this->get('/api/books?isbn='.$book['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0], $book); + } + + public function testValidateSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books?isbn=a', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['detail'], 'The isbn field must be at least 2 characters.'); + } + + public function testSearchFilterRelation(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books?author=1', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['author'], '/api/authors/1'); + } + + public function testPropertyFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + $response = $this->get(\sprintf('%s.jsonld?properties[]=author', $book['@id'])); + $book = $response->json(); + + $this->assertArrayHasKey('@id', $book); + $this->assertArrayHasKey('author', $book); + $this->assertArrayNotHasKey('name', $book); + } + + public function testPartialSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + if (!isset($book['name'])) { + throw new \UnexpectedValueException(); + } + + $end = strpos($book['name'], ' ') ?: 3; + $name = substr($book['name'], 0, $end); + + $response = $this->get('/api/books?name='.$name, ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0], $book); + } + + public function testDateFilterEqual(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => '2024-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[eq]='.$updated['publicationDate'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $book['@id']); + } + + public function testDateFilterIncludeNull(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => null], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationWithNulls[gt]=9999-12-31', ['Accept' => ['application/ld+json']]); + $this->assertGreaterThan(0, $response->json()['totalItems']); + } + + public function testDateFilterExcludeNull(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => null], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['Accept' => ['application/ld+json']]); + $this->assertSame(0, $response->json()['totalItems']); + } + + public function testDateFilterGreaterThan(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['publicationDate' => '9998-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['publicationDate' => '9999-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gt]='.$updated['publicationDate'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testDateFilterLowerThanEqual(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['publicationDate' => '0001-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['publicationDate' => '0002-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[lte]=0002-02-18', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testDateFilterBetween(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => '0001-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $book2 = $response->json()['member'][1]; + $this->patchJson( + $book2['@id'], + ['publicationDate' => '0002-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $book3 = $response->json()['member'][2]; + $updated3 = $this->patchJson( + $book3['@id'], + ['publicationDate' => '0003-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gte]='.substr($updated['publicationDate'], 0, 10).'&publicationDate[lt]='.substr($updated3['publicationDate'], 0, 10), ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $book['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $book2['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testSearchFilterWithPropertyPlaceholder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/authors', ['Accept' => ['application/ld+json']])->json(); + $author = $response['member'][0]; + + $test = $this->get('/api/authors?name='.explode(' ', $author['name'])[0], ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($test['member'][0]['id'], $author['id']); + + $test = $this->get('/api/authors?id='.$author['id'], ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($test['member'][0]['id'], $author['id']); + } + + public function testOrderFilterWithPropertyPlaceholder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $res = $this->get('/api/authors?order[id]=desc', ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($res['member'][0]['id'], 10); + } + + public function testOrFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']])->json()['member']; + $book = $response[0]; + $book2 = $response[1]; + + $res = $this->get(\sprintf('/api/books?name2[]=%s&name2[]=%s', $book['name'], $book2['name']), ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($res['totalItems'], 2); + } + + public function testRangeLowerThanFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['isbn' => '12'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $updated = $this->patchJson( + $bookAfter['@id'], + ['isbn' => '15'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[lt]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testRangeLowerThanEqualFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['isbn' => '12'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $updated = $this->patchJson( + $bookAfter['@id'], + ['isbn' => '15'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[lte]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testRangeGreaterThanFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['isbn' => '999999999999998'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['isbn' => '999999999999999'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[gt]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testRangeGreaterThanEqualFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['isbn' => '999999999999998'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['isbn' => '999999999999999'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + $response = $this->get('api/books?isbn_range[gte]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $json = $response->json(); + $this->assertSame($json['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($json['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($json['totalItems'], 2); + } + + public function testWrongOrderFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $res = $this->get('/api/authors?order[name]=something', ['Accept' => ['application/ld+json']]); + $this->assertEquals($res->getStatusCode(), 422); + } + + public function testWithAccessor(): void + { + WithAccessorFactory::new()->create(); + $res = $this->get('/api/with_accessors/1', ['Accept' => ['application/ld+json']]); + $this->assertArraySubset(['name' => 'test'], $res->json()); + } +} diff --git a/src/Laravel/Tests/GraphQlAuthTest.php b/src/Laravel/Tests/GraphQlAuthTest.php new file mode 100644 index 00000000000..2af920dd618 --- /dev/null +++ b/src/Laravel/Tests/GraphQlAuthTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Attributes\DefineEnvironment; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\UserFactory; +use Workbench\Database\Factories\VaultFactory; + +class GraphQlAuthTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + protected function afterRefreshingDatabase(): void + { + UserFactory::new()->create(); + } + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.routes.middleware', ['auth:sanctum']); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + $config->set('api-platform.graphql.enabled', true); + }); + } + + public function testUnauthenticated(): void + { + $response = $this->postJson('/api/graphql', [], []); + $response->assertStatus(401); + } + + public function testAuthenticated(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->get('/api/graphql', ['accept' => ['text/html'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $response = $this->postJson('/api/graphql', [ + 'query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}', + ], [ + 'content-type' => 'application/json', + 'authorization' => 'Bearer '.$token, + ]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testPolicy(): void + { + VaultFactory::new()->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/graphql', ['query' => 'mutation { + updateVault(input: {secret: "secret", id: "/api/vaults/1"}) { + vault {id} + } + } +'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('errors', $data); + $this->assertEquals('Access Denied.', $data['errors'][0]['message']); + } + + /** + * @param Application $app + */ + protected function useProductionMode($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.routes.middleware', ['auth:sanctum']); + $config->set('api-platform.graphql.enabled', true); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + $config->set('app.debug', false); + }); + } + + #[DefineEnvironment('useProductionMode')] + public function testProductionError(): void + { + VaultFactory::new()->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/graphql', ['query' => 'mutation { + updateVault(input: {secret: "secret", id: "/api/vaults/1"}) { + vault {id} + } + } +'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('errors', $data); + $this->assertArrayNotHasKey('trace', $data['errors'][0]); + } +} diff --git a/src/Laravel/Tests/GraphQlTest.php b/src/Laravel/Tests/GraphQlTest.php new file mode 100644 index 00000000000..2c3889b84e1 --- /dev/null +++ b/src/Laravel/Tests/GraphQlTest.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class GraphQlTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.graphql.enabled', true); + }); + } + + public function testGetBooks(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->postJson('/api/graphql', ['query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}'], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testGetBooksWithPaginationAndOrder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->postJson('/api/graphql', ['query' => '{ + books(first: 3, order: {name: "desc"}) { + edges { + node { + id, name, publicationDate, author { id, name } + } + } + } +}'], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']['books']['edges']); + $this->assertArrayNotHasKey('errors', $data); + } +} diff --git a/src/Laravel/Tests/HalTest.php b/src/Laravel/Tests/HalTest.php new file mode 100644 index 00000000000..b73f9bcf03c --- /dev/null +++ b/src/Laravel/Tests/HalTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class HalTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonhal' => ['application/hal+json']]); + $config->set('api-platform.docs_formats', ['jsonhal' => ['application/hal+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetEntrypoint(): void + { + $response = $this->get('/api/', ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + + $this->assertJsonContains( + [ + '_links' => [ + 'self' => ['href' => '/api'], + 'book' => ['href' => '/api/books'], + 'post' => ['href' => '/api/posts'], + 'sluggable' => ['href' => '/api/sluggables'], + 'vault' => ['href' => '/api/vaults'], + 'author' => ['href' => '/api/authors'], + ], + ], + $response->json() + ); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/hal+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + '_links' => [ + 'first' => ['href' => '/api/books?page=1'], + 'self' => ['href' => '/api/books?page=1'], + 'last' => ['href' => '/api/books?page=2'], + ], + 'totalItems' => 10, + ], + $response->json() + ); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + 'name' => $book->name, // @phpstan-ignore-line + 'isbn' => $book->isbn, // @phpstan-ignore-line + '_links' => [ + 'self' => [ + 'href' => $iri, + ], + 'author' => [ + 'href' => '/api/authors/1', + ], + ], + ], + $response->json() + ); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/hal+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/Tests/JsonApiTest.php b/src/Laravel/Tests/JsonApiTest.php new file mode 100644 index 00000000000..95492a9b0c6 --- /dev/null +++ b/src/Laravel/Tests/JsonApiTest.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class JsonApiTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.resources', [app_path('Models'), app_path('ApiResource')]); + $config->set('app.debug', true); + }); + } + + public function testGetEntrypoint(): void + { + $response = $this->get('/api/', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $this->assertJsonContains( + [ + 'links' => [ + 'self' => 'http://localhost/api', + 'book' => 'http://localhost/api/books', + ], + ], + $response->json() + ); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $response->assertJsonFragment([ + 'links' => [ + 'self' => '/api/books?page=1', + 'first' => '/api/books?page=1', + 'last' => '/api/books?page=2', + 'next' => '/api/books?page=2', + ], + 'meta' => ['totalItems' => 10, 'itemsPerPage' => 5, 'currentPage' => 1], + ]); + $response->assertJsonCount(5, 'data'); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'type' => 'Book', + 'attributes' => [ + 'name' => $book->name, // @phpstan-ignore-line + ], + ], + ], $response->json()); + } + + public function testCreateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(201); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $this->assertJsonContains([ + 'data' => [ + 'type' => 'Book', + 'attributes' => [ + 'name' => 'Don Quichotte', + ], + ], + ], $response->json()); + $this->assertMatchesRegularExpression('~^/api/books/~', $response->json('data.id')); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(200); + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'attributes' => [ + 'name' => 'updated title', + ], + ], + ], $response->json()); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'attributes' => [ + 'name' => 'updated title', + ], + ], + ], $response->json()); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']); + $content = $response->json(); + $this->assertArrayHasKey('data', $content); + $this->assertArrayHasKey('relationships', $content['data']); + $this->assertArrayHasKey('relation', $content['data']['relationships']); + $this->assertArrayHasKey('data', $content['data']['relationships']['relation']); + } + + public function testValidateJsonApi(): void + { + $response = $this->postJson( + '/api/issue6745/rule_validations', + [ + 'data' => [ + 'type' => 'string', + 'attributes' => ['max' => 3], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $json = $response->json(); + $this->assertJsonContains([ + 'errors' => [ + [ + 'detail' => 'The prop field is required.', + 'title' => 'Validation Error', + 'status' => 422, + 'code' => '58350900e0fc6b8e/prop', + ], + [ + 'detail' => 'The max field must be less than 2.', + 'title' => 'Validation Error', + 'status' => 422, + 'code' => '58350900e0fc6b8e/max', + ], + ], + ], $json); + + $this->assertArrayHasKey('id', $json['errors'][0]); + $this->assertArrayHasKey('links', $json['errors'][0]); + $this->assertArrayHasKey('type', $json['errors'][0]['links']); + + $response = $this->postJson( + '/api/issue6745/rule_validations', + [ + 'data' => [ + 'type' => 'string', + 'attributes' => [ + 'prop' => 1, + 'max' => 1, + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(201); + } + + public function testNotFound(): void + { + $response = $this->get('/api/books/notfound', headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(404); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + + $this->assertJsonContains([ + 'links' => ['type' => '/errors/404'], + 'title' => 'An error occurred', + 'status' => 404, + 'detail' => 'Not Found', + ], $response->json()['errors'][0]); + } +} diff --git a/src/Laravel/Tests/JsonLdTest.php b/src/Laravel/Tests/JsonLdTest.php new file mode 100644 index 00000000000..46039ebe68e --- /dev/null +++ b/src/Laravel/Tests/JsonLdTest.php @@ -0,0 +1,340 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\CommentFactory; +use Workbench\Database\Factories\PostFactory; +use Workbench\Database\Factories\SluggableFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class JsonLdTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + }); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@id' => '/api/books', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $response->assertJsonCount(5, 'member'); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $response = $this->get($this->getIriFromResource($book), ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@id' => $this->getIriFromResource($book), + '@type' => 'Book', + 'name' => $book->name, // @phpstan-ignore-line + ]); + } + + public function testCreateBook(): void + { + AuthorFactory::new()->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'name' => 'Don Quichotte', + 'author' => $this->getIriFromResource($author), + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(201); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@type' => 'Book', + 'name' => 'Don Quichotte', + ]); + $this->assertMatchesRegularExpression('~^/api/books/~', $response->json('@id')); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + 'name' => 'updated title', + ]); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + '@id' => $iri, + 'name' => 'updated title', + ]); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/ld+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } + + public function testPatchBookAuthor(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $author = Author::find(2); + $authorIri = $this->getIriFromResource($author); + $response = $this->patchJson( + $iri, + [ + 'author' => $authorIri, + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + '@id' => $iri, + 'author' => $authorIri, + ]); + } + + public function testSkolemIris(): void + { + $response = $this->get('/api/outputs', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@type' => 'NotAResource', + 'name' => 'test', + ]); + + $this->assertMatchesRegularExpression('~^/api/.well-known/genid/~', $response->json('@id')); + } + + public function testSubresourceCollection(): void + { + PostFactory::new()->has(CommentFactory::new()->count(10))->count(10)->create(); + $response = $this->get('/api/posts', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + + $response->assertJsonFragment([ + '@context' => '/api/contexts/Post', + '@id' => '/api/posts', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $response->assertJsonCount(10, 'member'); + $postIri = $response->json('member.0.@id'); + $commentsIri = $response->json('member.0.comments'); + $this->assertMatchesRegularExpression('~^/api/posts/\d+/comments$~', $commentsIri); + $response = $this->get($commentsIri, ['accept' => 'application/ld+json']); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Comment', + '@id' => $commentsIri, + '@type' => 'Collection', + 'totalItems' => 10, + ]); + + $commentIri = $response->json('member.0.@id'); + $response = $this->get($commentIri, ['accept' => 'application/ld+json']); + $response->assertJsonFragment([ + '@id' => $commentIri, + 'post' => $postIri, + ]); + } + + public function testCreateNotValid(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'name' => 'Don Quichotte', + 'author' => $this->getIriFromResource($author), + 'isbn' => 'test@foo', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/ValidationError', + '@type' => 'ValidationError', + 'description' => 'The isbn field must only contain letters and numbers.', + ]); + + $violations = $response->json('violations'); + $this->assertCount(1, $violations); + $this->assertEquals($violations[0], ['propertyPath' => 'isbn', 'message' => 'The isbn field must only contain letters and numbers.']); + } + + public function testCreateNotValidPost(): void + { + $response = $this->postJson( + '/api/posts', + [ + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/ValidationError', + '@type' => 'ValidationError', + 'description' => 'The title field is required.', + ]); + + $violations = $response->json('violations'); + $this->assertCount(1, $violations); + $this->assertEquals($violations[0], ['propertyPath' => 'title', 'message' => 'The title field is required.']); + } + + public function testSluggable(): void + { + SluggableFactory::new()->count(10)->create(); + $response = $this->get('/api/sluggables', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Sluggable', + '@id' => '/api/sluggables', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $iri = $response->json('member.0.@id'); + $response = $this->get($iri, ['accept' => 'application/ld+json']); + $response->assertStatus(200); + } + + public function testApiDocsRegex(): void + { + $response = $this->get('/api/notexists', ['accept' => 'application/ld+json']); + $response->assertNotFound(); + } + + public function testHidden(): void + { + PostFactory::new()->has(CommentFactory::new()->count(10))->count(10)->create(); + $response = $this->get('/api/posts/1/comments/1', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonMissingPath('internalNote'); + } + + public function testVisible(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $this->assertStringNotContainsString('internalNote', (string) $response->getContent()); + } + + public function testError(): void + { + $response = $this->post('/api/books', ['content-type' => 'application/vnd.api+json']); + $response->assertStatus(415); + $content = $response->json(); + $this->assertArrayHasKey('trace', $content); + } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']); + $content = $response->json(); + $this->assertArrayHasKey('relation', $content); + $this->assertArrayHasKey('name', $content['relation']); + } +} diff --git a/src/Laravel/Tests/JsonProblemTest.php b/src/Laravel/Tests/JsonProblemTest.php new file mode 100644 index 00000000000..ea91b2f84ee --- /dev/null +++ b/src/Laravel/Tests/JsonProblemTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Attributes\DefineEnvironment; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class JsonProblemTest extends TestCase +{ + use RefreshDatabase; + use WithWorkbench; + + public function testNotFound(): void + { + $response = $this->get('/api/books/notfound', headers: ['accept' => 'application/ld+json']); + $response->assertStatus(404); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Error', + '@id' => '/api/errors/404.jsonld', + '@type' => 'hydra:Error', + 'type' => '/errors/404', + 'title' => 'An error occurred', + 'status' => 404, + 'detail' => 'Not Found', + ]); + } + + /** + * @param Application $app + */ + protected function useProductionMode($app): void + { + $app['config']->set('app.debug', false); + } + + #[DefineEnvironment('useProductionMode')] + public function testProductionError(): void + { + $response = $this->post('/api/books', ['content-type' => 'application/vnd.api+json']); + $response->assertStatus(415); + $content = $response->json(); + $this->assertArrayNotHasKey('trace', $content); + $this->assertArrayNotHasKey('line', $content); + $this->assertArrayNotHasKey('file', $content); + } +} diff --git a/src/Laravel/Tests/LinkHeaderTest.php b/src/Laravel/Tests/LinkHeaderTest.php new file mode 100644 index 00000000000..4aa1a41e9c5 --- /dev/null +++ b/src/Laravel/Tests/LinkHeaderTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class LinkHeaderTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testLinkHeader(): void + { + $response = $this->get('/api/', ['accept' => ['application/ld+json']]); + $response->assertStatus(200); + $response->assertHeader('link', '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'); + } +} diff --git a/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php b/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php new file mode 100644 index 00000000000..edc2178f24f --- /dev/null +++ b/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class LinkHeaderWithoutJsonldTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + }); + } + + public function testLinkHeader(): void + { + $response = $this->get('/api/', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeaderMissing('link'); + } +} diff --git a/src/Laravel/Tests/Policy/BookAllowPolicy.php b/src/Laravel/Tests/Policy/BookAllowPolicy.php new file mode 100644 index 00000000000..bea0109c494 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookAllowPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookAllowPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return true; + } +} diff --git a/src/Laravel/Tests/Policy/BookDenyPolicy.php b/src/Laravel/Tests/Policy/BookDenyPolicy.php new file mode 100644 index 00000000000..d9f70e82930 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookDenyPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookDenyPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return false; + } +} diff --git a/src/Laravel/Tests/Policy/PolicyAllowTest.php b/src/Laravel/Tests/Policy/PolicyAllowTest.php new file mode 100644 index 00000000000..c07eeb7a459 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyAllowTest.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class PolicyAllowTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookAllowPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testGetEmptyCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertJsonFragment([ + 'meta' => [ + 'totalItems' => 0, + 'itemsPerPage' => 5, + 'currentPage' => 1, + ], + ]); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testCreateBook(): void + { + AuthorFactory::new()->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(201); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(200); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/Tests/Policy/PolicyDenyTest.php b/src/Laravel/Tests/Policy/PolicyDenyTest.php new file mode 100644 index 00000000000..dfdf114ae41 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyDenyTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class PolicyDenyTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookDenyPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testCreateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(403); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(403); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(403); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(403); + } +} diff --git a/src/Laravel/composer.json b/src/Laravel/composer.json new file mode 100644 index 00000000000..65c1ef6be14 --- /dev/null +++ b/src/Laravel/composer.json @@ -0,0 +1,122 @@ +{ + "name": "api-platform/laravel", + "description": "API Platform support for Laravel", + "keywords": [ + "Laravel", + "REST", + "GraphQL", + "API", + "JSON-LD", + "Hydra", + "JSONAPI", + "OpenAPI", + "HAL", + "Swagger" + ], + "homepage": "https://api-platform.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.2", + "api-platform/documentation": "^4.0", + "api-platform/hydra": "^4.0", + "api-platform/json-hal": "^4.0", + "api-platform/json-schema": "^4.0", + "api-platform/jsonld": "^4.0", + "api-platform/json-api": "^4.0", + "api-platform/metadata": "^4.0", + "api-platform/openapi": "^4.0", + "api-platform/serializer": "^4.0", + "api-platform/state": "^4.0", + "illuminate/config": "^11.0", + "laravel/framework": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/http": "^11.0", + "illuminate/pagination": "^11.0", + "illuminate/routing": "^11.0", + "illuminate/support": "^11.0", + "illuminate/container": "^11.0", + "symfony/web-link": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.1", + "phpstan/phpdoc-parser": "^1.29", + "phpdocumentor/reflection-docblock": "^5.1" + }, + "require-dev": { + "doctrine/dbal": "^4.0", + "larastan/larastan": "^2.0", + "orchestra/testbench": "^9.1", + "phpunit/phpunit": "^11.2", + "api-platform/graphql": "^4.0", + "laravel/sanctum": "^4.0" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Laravel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/workbench/" + ] + }, + "config": { + "sort-packages": true + }, + "suggest": { + "api-platform/graphql": "Enable GraphQl support.", + "phpdocumentor/reflection-docblock": "" + }, + "extra": { + "laravel": { + "providers": [ + "ApiPlatform\\Laravel\\ApiPlatformProvider" + ] + }, + "branch-alias": { + "dev-main": "4.0.x-dev", + "dev-3.4": "3.4.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "autoload-dev": { + "psr-4": { + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } + }, + "scripts": { + "build": "@php vendor/bin/testbench workbench:build --ansi", + "test": "@php vendor/bin/testbench package:test", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve --ansi" + ], + "lint": [ + "@php vendor/bin/phpstan analyse --verbose --ansi" + ] + } +} diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php new file mode 100644 index 00000000000..bd1db3114ad --- /dev/null +++ b/src/Laravel/config/api-platform.php @@ -0,0 +1,100 @@ + 'API Platform', + 'description' => 'My awesome API', + 'version' => '1.0.0', + 'show_webby' => true, + + 'routes' => [ + // Global middleware applied to every API Platform routes + // 'middleware' => [] + ], + + 'resources' => [ + app_path('Models'), + ], + + 'formats' => [ + 'jsonld' => ['application/ld+json'], + //'jsonapi' => ['application/vnd.api+json'], + //'csv' => ['text/csv'], + ], + + 'patch_formats' => [ + 'json' => ['application/merge-patch+json'], + ], + + 'docs_formats' => [ + 'jsonld' => ['application/ld+json'], + //'jsonapi' => ['application/vnd.api+json'], + 'jsonopenapi' => ['application/vnd.openapi+json'], + 'html' => ['text/html'], + ], + + 'error_formats' => [ + 'jsonproblem' => ['application/problem+json'], + ], + + 'defaults' => [ + 'pagination_enabled' => true, + 'pagination_partial' => false, + 'pagination_client_enabled' => false, + 'pagination_client_items_per_page' => false, + 'pagination_client_partial' => false, + 'pagination_items_per_page' => 30, + 'pagination_maximum_items_per_page' => 30, + 'route_prefix' => '/api', + 'middleware' => [], + ], + + 'pagination' => [ + 'page_parameter_name' => 'page', + 'enabled_parameter_name' => 'pagination', + 'items_per_page_parameter_name' => 'itemsPerPage', + 'partial_parameter_name' => 'partial', + ], + + 'graphql' => [ + 'enabled' => false, + 'nesting_separator' => '__', + 'introspection' => ['enabled' => true] + ], + + 'exception_to_status' => [ + AuthenticationException::class => 401, + AuthorizationException::class => 403 + ], + + 'swagger_ui' => [ + 'enabled' => true, + //'apiKeys' => [ + // 'api' => [ + // 'type' => 'Bearer', + // 'name' => 'Authentication Token', + // 'in' => 'header' + // ] + //], + //'oauth' => [ + // 'enabled' => true, + // 'type' => 'oauth2', + // 'flow' => 'authorizationCode', + // 'tokenUrl' => '', + // 'authorizationUrl' =>'', + // 'refreshUrl' => '', + // 'scopes' => ['scope1' => 'Description scope 1'], + // 'pkce' => true + //] + ], + + 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, + + 'serializer' => [ + 'hydra_prefix' => false, + // 'datetime_format' => \DateTimeInterface::RFC3339 + ] +]; diff --git a/src/Laravel/phpstan.neon.dist b/src/Laravel/phpstan.neon.dist new file mode 100644 index 00000000000..4bae0676110 --- /dev/null +++ b/src/Laravel/phpstan.neon.dist @@ -0,0 +1,22 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + level: 7 + paths: + - ApiResource + - config + - Controller + - Eloquent + - Exception + - Routing + - State + - Test + - Tests + ignoreErrors: + - '#Cannot call method expectsQuestion#' +# +# excludePaths: +# - ./*/*/FileToBeExcluded.php +# +# checkMissingIterableValueType: false diff --git a/src/ParameterValidator/phpunit.xml.dist b/src/Laravel/phpunit.xml.dist similarity index 52% rename from src/ParameterValidator/phpunit.xml.dist rename to src/Laravel/phpunit.xml.dist index 3a36ca8b8cb..b3326b6b5d6 100644 --- a/src/ParameterValidator/phpunit.xml.dist +++ b/src/Laravel/phpunit.xml.dist @@ -1,24 +1,23 @@ - + - - + ./Tests/ - - + + ./ @@ -26,5 +25,5 @@ ./Tests ./vendor - + diff --git a/src/Laravel/public/400.css b/src/Laravel/public/400.css new file mode 100644 index 00000000000..4a3ee47e66f --- /dev/null +++ b/src/Laravel/public/400.css @@ -0,0 +1,79 @@ +/* open-sans-cyrillic-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-cyrillic-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-ext-400-normal.woff) format('woff'); + unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +/* open-sans-cyrillic-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-cyrillic-400-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-400-normal.woff) format('woff'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +/* open-sans-greek-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-greek-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-greek-ext-400-normal.woff) format('woff'); + unicode-range: U+1F00-1FFF; +} + +/* open-sans-greek-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-greek-400-normal.woff2) format('woff2'), url(./files/open-sans-greek-400-normal.woff) format('woff'); + unicode-range: U+0370-03FF; +} + +/* open-sans-hebrew-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-hebrew-400-normal.woff2) format('woff2'), url(./files/open-sans-hebrew-400-normal.woff) format('woff'); + unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; +} + +/* open-sans-vietnamese-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-vietnamese-400-normal.woff2) format('woff2'), url(./files/open-sans-vietnamese-400-normal.woff) format('woff'); + unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; +} + +/* open-sans-latin-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-latin-ext-400-normal.woff2) format('woff2'), url(./files/open-sans-latin-ext-400-normal.woff) format('woff'); + unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +/* open-sans-latin-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(./files/open-sans-latin-400-normal.woff2) format('woff2'), url(./files/open-sans-latin-400-normal.woff) format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} \ No newline at end of file diff --git a/src/Laravel/public/700.css b/src/Laravel/public/700.css new file mode 100644 index 00000000000..f20bbcf0684 --- /dev/null +++ b/src/Laravel/public/700.css @@ -0,0 +1,79 @@ +/* open-sans-cyrillic-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-cyrillic-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-ext-700-normal.woff) format('woff'); + unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +/* open-sans-cyrillic-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-cyrillic-700-normal.woff2) format('woff2'), url(./files/open-sans-cyrillic-700-normal.woff) format('woff'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +/* open-sans-greek-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-greek-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-greek-ext-700-normal.woff) format('woff'); + unicode-range: U+1F00-1FFF; +} + +/* open-sans-greek-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-greek-700-normal.woff2) format('woff2'), url(./files/open-sans-greek-700-normal.woff) format('woff'); + unicode-range: U+0370-03FF; +} + +/* open-sans-hebrew-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-hebrew-700-normal.woff2) format('woff2'), url(./files/open-sans-hebrew-700-normal.woff) format('woff'); + unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; +} + +/* open-sans-vietnamese-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-vietnamese-700-normal.woff2) format('woff2'), url(./files/open-sans-vietnamese-700-normal.woff) format('woff'); + unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; +} + +/* open-sans-latin-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-latin-ext-700-normal.woff2) format('woff2'), url(./files/open-sans-latin-ext-700-normal.woff) format('woff'); + unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +/* open-sans-latin-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(./files/open-sans-latin-700-normal.woff2) format('woff2'), url(./files/open-sans-latin-700-normal.woff) format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} \ No newline at end of file diff --git a/src/Laravel/public/car.svg b/src/Laravel/public/car.svg new file mode 100644 index 00000000000..b74b16f85fb --- /dev/null +++ b/src/Laravel/public/car.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Laravel/public/es6-promise/es6-promise.auto.min.js b/src/Laravel/public/es6-promise/es6-promise.auto.min.js new file mode 100644 index 00000000000..fdf8bff2676 --- /dev/null +++ b/src/Laravel/public/es6-promise/es6-promise.auto.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){B=t}function r(t){G=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof z?function(){z(a)}:c()}function s(){var t=0,e=new J(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t this.readyToRead) { + push = USE_ALLOC ? Buffer.alloc(this.readyToRead, '', 'binary') : new Buffer(this.readyToRead, 'binary'); + this.responseBuffer.copy(push, 0, 0, this.readyToRead); + restSize = this.responseBuffer.length - this.readyToRead; + rest = USE_ALLOC ? Buffer.alloc(restSize, '', 'binary') : new Buffer(restSize, 'binary'); + this.responseBuffer.copy(rest, 0, this.readyToRead); + } else { + push = this.responseBuffer; + rest = USE_ALLOC ? Buffer.alloc(0, '', 'binary') : new Buffer(0, 'binary'); + } + this.responseBuffer = rest; + this.readyToRead = 0; + if (this.options.encoding) { + this.push(push, this.options.encoding); + } else { + this.push(push); + } +}; + +FetchStream.prototype.destroy = function (ex) { + this.emit('destroy', ex); +}; + +FetchStream.prototype.normalizeOptions = function () { + + // cookiejar + this.cookieJar = this.options.cookieJar || new CookieJar(); + + // default redirects - 10 + // if disableRedirect is set, then 0 + if (!this.options.disableRedirect && typeof this.options.maxRedirects !== 'number' && + !(this.options.maxRedirects instanceof Number)) { + this.options.maxRedirects = 10; + } else if (this.options.disableRedirects) { + this.options.maxRedirects = 0; + } + + // normalize header keys + // HTTP and HTTPS takes in key names in case insensitive but to find + // an exact value from an object key name needs to be case sensitive + // so we're just lowercasing all input keys + this.options.headers = this.options.headers || {}; + + var keys = Object.keys(this.options.headers); + var newheaders = {}; + var i; + + for (i = keys.length - 1; i >= 0; i--) { + newheaders[keys[i].toLowerCase().trim()] = this.options.headers[keys[i]]; + } + + this.options.headers = newheaders; + + if (!this.options.headers['user-agent']) { + this.options.headers['user-agent'] = this.userAgent; + } + + if (!this.options.headers.pragma) { + this.options.headers.pragma = 'no-cache'; + } + + if (!this.options.headers['cache-control']) { + this.options.headers['cache-control'] = 'no-cache'; + } + + if (!this.options.disableGzip) { + this.options.headers['accept-encoding'] = 'gzip, deflate'; + } else { + delete this.options.headers['accept-encoding']; + } + + // max length for the response, + // if not set, default is Infinity + if (!this.options.maxResponseLength) { + this.options.maxResponseLength = Infinity; + } + + // method: + // defaults to GET, or when payload present to POST + if (!this.options.method) { + this.options.method = this.options.payload || this.options.payloadSize ? 'POST' : 'GET'; + } + + // set cookies + // takes full cookie definition strings as params + if (this.options.cookies) { + for (i = 0; i < this.options.cookies.length; i++) { + this.cookieJar.setCookie(this.options.cookies[i], this.url); + } + } + + // rejectUnauthorized + if (typeof this.options.rejectUnauthorized === 'undefined') { + this.options.rejectUnauthorized = true; + } +}; + +FetchStream.prototype.parseUrl = function (url) { + var urlparts = urllib.parse(url, false, true), + transport, + urloptions = { + host: urlparts.hostname || urlparts.host, + port: urlparts.port, + path: urlparts.pathname + (urlparts.search || '') || '/', + method: this.options.method, + rejectUnauthorized: this.options.rejectUnauthorized + }; + + switch (urlparts.protocol) { + case 'https:': + transport = https; + break; + case 'http:': + default: + transport = http; + break; + } + + if (transport === https) { + if('agentHttps' in this.options){ + urloptions.agent = this.options.agentHttps; + } + if('agent' in this.options){ + urloptions.agent = this.options.agent; + } + } else { + if('agentHttp' in this.options){ + urloptions.agent = this.options.agentHttp; + } + if('agent' in this.options){ + urloptions.agent = this.options.agent; + } + } + + if (!urloptions.port) { + switch (urlparts.protocol) { + case 'https:': + urloptions.port = 443; + break; + case 'http:': + default: + urloptions.port = 80; + break; + } + } + + urloptions.headers = this.options.headers || {}; + + if (urlparts.auth) { + var buf = USE_ALLOC ? Buffer.alloc(Buffer.byteLength(urlparts.auth), urlparts.auth) : new Buffer(urlparts.auth); + urloptions.headers.Authorization = 'Basic ' + buf.toString('base64'); + } + + return { + urloptions: urloptions, + transport: transport + }; +}; + +FetchStream.prototype.setEncoding = function (encoding) { + this.options.encoding = encoding; +}; + +FetchStream.prototype.runStream = function (url) { + var url_data = this.parseUrl(url), + cookies = this.cookieJar.getCookies(url); + + if (cookies) { + url_data.urloptions.headers.cookie = cookies; + } else { + delete url_data.urloptions.headers.cookie; + } + + if (this.options.payload) { + url_data.urloptions.headers['content-length'] = Buffer.byteLength(this.options.payload || '', 'utf-8'); + } + + if (this.options.payloadSize) { + url_data.urloptions.headers['content-length'] = this.options.payloadSize; + } + + if (this.options.asyncDnsLoookup) { + var dnsCallback = (function (err, addresses) { + if (err) { + this.emit('error', err); + return; + } + + url_data.urloptions.headers.host = url_data.urloptions.hostname || url_data.urloptions.host; + url_data.urloptions.hostname = addresses[0]; + url_data.urloptions.host = url_data.urloptions.headers.host + (url_data.urloptions.port ? ':' + url_data.urloptions.port : ''); + + this._runStream(url_data, url); + }).bind(this); + + if (net.isIP(url_data.urloptions.host)) { + dnsCallback(null, [url_data.urloptions.host]); + } else { + dns.resolve4(url_data.urloptions.host, dnsCallback); + } + } else { + this._runStream(url_data, url); + } +}; + +FetchStream.prototype._runStream = function (url_data, url) { + + var req = url_data.transport.request(url_data.urloptions, (function (res) { + + // catch new cookies before potential redirect + if (Array.isArray(res.headers['set-cookie'])) { + for (var i = 0; i < res.headers['set-cookie'].length; i++) { + this.cookieJar.setCookie(res.headers['set-cookie'][i], url); + } + } + + if ([301, 302, 303, 307, 308].indexOf(res.statusCode) >= 0) { + if (!this.options.disableRedirects && this.options.maxRedirects > this._redirect_count && res.headers.location) { + this._redirect_count++; + req.destroy(); + this.runStream(urllib.resolve(url, res.headers.location)); + return; + } + } + + this.meta = { + status: res.statusCode, + responseHeaders: res.headers, + finalUrl: url, + redirectCount: this._redirect_count, + cookieJar: this.cookieJar + }; + + var curlen = 0, + maxlen, + + receive = (function (chunk) { + if (curlen + chunk.length > this.options.maxResponseLength) { + maxlen = this.options.maxResponseLength - curlen; + } else { + maxlen = chunk.length; + } + + if (maxlen <= 0) { + return; + } + + curlen += Math.min(maxlen, chunk.length); + if (maxlen >= chunk.length) { + if (this.responseBuffer.length === 0) { + this.responseBuffer = chunk; + } else { + this.responseBuffer = Buffer.concat([this.responseBuffer, chunk]); + } + } else { + this.responseBuffer = Buffer.concat([this.responseBuffer, chunk], this.responseBuffer.length + maxlen); + } + this.drainBuffer(); + }).bind(this), + + error = (function (e) { + this.ended = true; + this.emit('error', e); + this.drainBuffer(); + }).bind(this), + + end = (function () { + this.ended = true; + if (this.responseBuffer.length === 0) { + this.push(null); + } + }).bind(this), + + unpack = (function (type, res) { + var z = zlib['create' + type](); + z.on('data', receive); + z.on('error', error); + z.on('end', end); + res.pipe(z); + }).bind(this); + + this.emit('meta', this.meta); + + if (res.headers['content-encoding']) { + switch (res.headers['content-encoding'].toLowerCase().trim()) { + case 'gzip': + return unpack('Gunzip', res); + case 'deflate': + return unpack('InflateRaw', res); + } + } + + res.on('data', receive); + res.on('end', end); + + }).bind(this)); + + req.on('error', (function (e) { + this.emit('error', e); + }).bind(this)); + + if (this.options.timeout) { + req.setTimeout(this.options.timeout, req.abort.bind(req)); + } + this.on('destroy', req.abort.bind(req)); + + if (this.options.payload) { + req.end(this.options.payload); + } else if (this.options.payloadStream) { + this.options.payloadStream.pipe(req); + this.options.payloadStream.resume(); + } else { + req.end(); + } +}; + +function fetchUrl(url, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = undefined; + } + options = options || {}; + + var fetchstream = new FetchStream(url, options), + response_data, chunks = [], + length = 0, + curpos = 0, + buffer, + content_type, + callbackFired = false; + + fetchstream.on('meta', function (meta) { + response_data = meta; + content_type = _parseContentType(meta.responseHeaders['content-type']); + }); + + fetchstream.on('data', function (chunk) { + if (chunk) { + chunks.push(chunk); + length += chunk.length; + } + }); + + fetchstream.on('error', function (error) { + if (error && error.code === 'HPE_INVALID_CONSTANT') { + // skip invalid formatting errors + return; + } + if (callbackFired) { + return; + } + callbackFired = true; + callback(error); + }); + + fetchstream.on('end', function () { + if (callbackFired) { + return; + } + callbackFired = true; + + buffer = USE_ALLOC ? Buffer.alloc(length) : new Buffer(length); + for (var i = 0, len = chunks.length; i < len; i++) { + chunks[i].copy(buffer, curpos); + curpos += chunks[i].length; + } + + if (content_type.mimeType === 'text/html') { + content_type.charset = _findHTMLCharset(buffer) || content_type.charset; + } + + content_type.charset = (options.overrideCharset || content_type.charset || 'utf-8').trim().toLowerCase(); + + + if (!options.disableDecoding && !content_type.charset.match(/^utf-?8$/i)) { + buffer = encodinglib.convert(buffer, 'UTF-8', content_type.charset); + } + + if (options.outputEncoding) { + return callback(null, response_data, buffer.toString(options.outputEncoding)); + } else { + return callback(null, response_data, buffer); + } + + }); +} + +function _parseContentType(str) { + if (!str) { + return {}; + } + var parts = str.split(';'), + mimeType = parts.shift(), + charset, chparts; + + for (var i = 0, len = parts.length; i < len; i++) { + chparts = parts[i].split('='); + if (chparts.length > 1) { + if (chparts[0].trim().toLowerCase() === 'charset') { + charset = chparts[1]; + } + } + } + + return { + mimeType: (mimeType || '').trim().toLowerCase(), + charset: (charset || 'UTF-8').trim().toLowerCase() // defaults to UTF-8 + }; +} + +function _findHTMLCharset(htmlbuffer) { + + var body = htmlbuffer.toString('ascii'), + input, meta, charset; + + if ((meta = body.match(/]*?>/i))) { + input = meta[0]; + } + + if (input) { + charset = input.match(/charset\s?=\s?([a-zA-Z\-0-9]*);?/); + if (charset) { + charset = (charset[1] || '').trim().toLowerCase(); + } + } + + if (!charset && (meta = body.match(/li.selected, +#graphiql .graphiql-container .toolbar-menu-items>li.hover, +#graphiql.graphiql-container .toolbar-menu-items>li:active, +#graphiql .graphiql-container .toolbar-menu-items>li:hover, +#graphiql.graphiql-container .toolbar-select-options>li.hover, +#graphiql .graphiql-container .toolbar-select-options>li:active, +#graphiql .graphiql-container .toolbar-select-options>li:hover, +#graphiql .graphiql-container .history-contents>p:hover, +#graphiql .graphiql-container .history-contents>p:active { + background: #288690; +} diff --git a/src/Laravel/public/graphiql/graphiql.css b/src/Laravel/public/graphiql/graphiql.css new file mode 100644 index 00000000000..e99012ff6f5 --- /dev/null +++ b/src/Laravel/public/graphiql/graphiql.css @@ -0,0 +1,641 @@ +/*!*********************************************************************************************!*\ + !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/font/roboto.css ***! + \*********************************************************************************************/ +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMwAA4AAAAABZgAAALdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI4ghsLEAABNgIkAxwEIAWDCgcgG3YEyI7DdHsjE9IUV+CFDh74vPL9/MmgO0un0soqjWt7En2kQoCMtXsRxyxkMqP9iO6NfSiUaLJuoRIKnhI0+ImbcWOB5XOAFVmCgxZQQmuBJRhZtsUCXm/492Dyuk2YZJdkdApZeOzyEQgKOwDgRjASBEEBVmAlgACtOHEhpjLyyrACMAB0vaLa6cAw5bc5bvhA2uwO7zXAyKPmkYNnAJgBxLEMDxFLqVBPI6EQ/daTr/QOAgfCngRoZc4UZiL623qCkf/oHVsfRCOuAIbJyF4ajQQKQLmQhNBAA4aygH9b19Xw4iAC8DkKM6WrYw/ABMAOWEAamA7sgBWACgAUSlc3SCmlc95o45idYD92Qt/+5gF19v3FALtB9+7dq/h6/Ljyu/zzYfnngwdlHxO+k39nOcO/e7nPf2vCoo3HVlmNTdnWwW3JZffuVU6cQX14kb3qUGOOJ+mjP9iMeb1Nivq5gXpJUWm+cmVK56e6PjI2uce23hHlG48vyDvym5/5q+wbkjq90rN+z53D6zXqmVUPVshZoVtrZgc4vleS1NNrni6VR8I/vTrpzpPwu1+1Pel4xBIzK16W3KcLNnVGl2RGZHbPXBAvhw4M02Ci/t0BBfw/p79XS9V7CKAMF0++DK9rtI/7MXvGATjz0TEA4K4oef476t9dS555BAoLBYCA6ei/FSzVgvg/cIR45gpTaLWeLiB+oa4xJuTks7r7/xwCmCzlpoJKALCDQmkyEsCsN0mELUADghGsGgAF6c9IXkabDYyqg6WMkZd9z7BT5gaphhhqnOH66aOvkTQhggQLpsk0xBB9DNSLJttgPQTQJBtoIE0JEY2wb+1lhF6GG62XngKUGKLFECMNkW2kZgP10+M31GZUwfojwkU0uAcQkISKFNtqGMlau3vIjjRUjMANjYkDNKeouYh7CRBmuD4CHQgHG6GXET8oT7ZU6QqUStddiABBJPSv6P315AAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAANUAA4AAAAABbwAAAMBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI0ghgLEAABNgIkAxwEIAWDMgcgG5sECK4GbGM62A+KOMNGmZWUwcdhKI9l4Sh/WwYP/3af9w0W4ERa2bOg405uoSptTooGKkF8HniO5b+Iojvye4dReBbNtVHwcLQTG2gBzQfYOqjJ/XYU/jItwgxa4I3czM4Fj9LAAnlHz+dzgSO71Jqn2QML8H66dROj0qAFLYnRhtm0b89/erW/v8l/LA6we9gCizDBtQzSf4EtkcwDT6RtmgYEQXnDKGQslZyX/CkQSFgBAE4ERggEAgmwACwQgADMsONAJKVkFWEBgAJgwMz1NlLWec3G+jtZu+rXO1i7rx/sZi0AEwB5WVY28FUE1CORQAjvtSPftAwCQQjGAbTUfm4qwrvbNmDEf5pjR4JoxElAiYiMWjQyIAEy4EBGAA4UNKCgIMC7a5Cej2sCAA+SMEEyYA2AMQBWgCmQAObACrAAQAUAJCSDMEDmo7CztfXoRGu7SUeVdbvosOq6N6PHnZ2yf9l3eXPj/q2qXdkjBL+qrix1cYsqzItOvXfRPaMXkUvPeFWoxr7tZB8gfxIhMauBapmSUhO8d3O8wUt0MoI7UAxLzt0/zhCwJnVHrsPYXenm8suPeLYORWqn/3wwK6Qp+frDiYGvxHSXFzoXfpihfmlODl9oFbOqKa8nXbZgd6axNivh4JS8xEZKChij/nuDBPx/MrxQA/WBACCtK44947xa66g/k0YcALjxaesDuBuQP/7x/3bTwmQACVMkAAQYd/7HYBqK1H97hriqWIzlN7cD8Qu1mY6Ql7eR9v8qAcCY/apKqAgArEBCCmOEAExoJiOUENTgBAI3NSBhwSjIbLboV0Blo3PIiN06hxVFfmrr0WtMvzYtWg3SBPDjz58mVY8eLTrpNOm6NfKhidepk6ZAbgbym+oG6PoN0zXxUaBHgx6Demiy6Zq0GdIl3aB6ndo04r7WvSV0/Qa0Nd2+yKcNFCrSvh/6dNKO3xV33aBeEXxNZKTyQUaverfOR49+LZno1XUboBt4oSzpEiXLUSjZDgF8+JHBMIY0KQAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAALsAA4AAAAABWAAAAKbAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXsLEAABNgIkAxwEIAWCdAcgG0AEAB6HcYyyEjO2Dy0eKLv4XvfsrGs+wIhEBOHOERRRTI2158fc/aln0WYmSJq8uTRSIgUyIVMqpfa/7uYHCqzWDuHREj0f5UuuL+ZAokTaYgiIs5sF5aUutjO7QhBlgMaYvCAIIqqoCggoq0+HjRlX70MGclDLyR3Z8fb0q/ectzCv30obmLesvO5hBhRhcp7kToaLpaRXpL0htKmb5C3rIgzUIwA1fnqrhHSbqXhA3v+sK1wRtcWuhdyg9E5tGXERkaAhroCGeNqCnJxAm6m1Sb58SICvFhXFWnVAAWQoYRjYADJUQQqIYm0uSZKkfpYv1sv21dm9b7kWbV6i3BQ2Z/sOf/hl+ezXH88LRz75pnLuq4/MO/Zx+eyHc3x9VDn3yfx9n1ILyusq3ps75y90fVZ657PJ2iXgF+odHbvzv7Lrm+uTsPR0WJqYcelN7180rHDDnbeWbrx0QHht49uXjCzffOsd5RsvGvHe4yF5o+Ej97/ZMP62+Z+3Wz/08CtZ/FezhpdvG/nb6PMhC9vNvHFx3Du9X47etewROuONg4L0v2eI+L9X7dt0evq+gNihfvWttiuWK4f8VmxWBM/+WK8b8F6Y9evfLf57r9SjuA2URBAobPm/Smni3y3+n1TqgQEACsl5awAI/5AetjNp65A+/38vDAUXaayPL4CMKHYkEFC0DlfIlbAMegyqlmGU2eSTO58TTHX2xLyWvlczc/wY7eDo5WxlYenKyMvNg9Go5MAatqis2Jty2oytLaPupFxOlsgFObsjM05dBxMHVwcMbeFma4xFh8jZxUr2e62Th09I7Bd96I2RI3gzYzqKcsHjqZzGjsamlojTwdmCy9bKFNm7IBcudRU5BU09BQ5eTm5coMaMAw==) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMAAA4AAAAABWwAAAKuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXkLEAABNgIkAxwEIAWDAAcgG0oEAB6D426JQgSiDJGrY+EepR5ejwf4/fWd+/C1EBKYZDS7sRFxHTf9uCJn/m9Of4qsOwRQBbqEex0QSbKziM9Pj42dA85/tYTLU84Cj+f+PIAlq3AtV5GCrQWUqr11TNFedSEUjKs7rSju46fX7RWCSHFAeYQcQRBEKIqiAgIKlGZBdO5a3w4akEBWj6orkgSzThrq5iF0WjfiKGe7e/0dAHkwOR8nW+GblHR72hyEGmzEl02NcDPu9oBKt35NVVBcoyEuIJNhau72SE3EHkhapkdqCiZGhBhliQWUJVETSCQCNfr8o/boWoBjI3miLHqQC4ojH22AaUBxFAUpIBJlJeIVGIvLFI6PlFi4hGYVs0brZ4ZZlT0rbz1SLT+50xlW3X269vh2x+CpO/n7bw02ebvIys0wMkpteMHUIq4PGfxCRBdKjxXGaDRIc42rK+a/qgeebsfBvjGMiQ14cnJjW8fSe6fHlr2NIrgbeH2jS+k9X+md9WJP/5IvZ8LRg1cQ3gz+dJMePnr2/6ZSiy3c9rHc87Zj4tqOx0WLe1U0VR2OOEt9kq4gV/r/NBEyVbPvpL70poCoTunu3LVVZ4nW3xWV8gAKP5VqBMD10Pruq+7/52x5c4B8EQjkzs5oyJ/1JzxT0mgEACA3XjUZACFDut7UuAEqPZepikCuTcprJBVAcSJREzIBeaYSC4kSGAs2BJU5IFLcQjt+sxNAqr55kwOx947iBrvVCRYwpBuDQusVLFWyFCmCVcEwCg8JVsPPK1GwEjxesNZJv6dyHtID6dYP8UnUCvPAemHBGiA+jD6CVgilD8+tWyfSPRiYXwVJDNNkydPUzvrRmeBZvFdArqSTDSCJ3ALcvDp0JBHWjTK8pb0Qvx7N35CkXo0yFRq1qZAgVaJkYiA7H3AA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAABK8AA4AAAAAIgAAABJmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmQbi3YcNgZgAIFkEQwKqUCgdAuBSAABNgIkA4MMBCAFgwAHIBv5G7MREWwcAAjqiQT/ZYJtzPyxTqRrsF1IYVrRiFiApETA1++dMFq11kZtOhdxHMTvna14XthLn3dGSDLLg/3yf+feJLvv07tDOZClulqMQCikLU04jMMxKJjN/62Zf2Zn6Q/sAXIBXSvkMaRJCZJ8M3t1ycm+ClNhKzzhQnWV6OBa295MdqJv5linkmiJxg/83P7PZUGHMCpH9J/UqI7hqE/HyFAf5qgQjBlEGRlMe0AB/E+trYhYqhYSodDoJpHmFSLRpl9DxF99b+bPbd/9Mul3vXfutinJdmq2SYcgiepGYMWE4fI/gv9/7tXmntsM+A1QMfsJvRlBau7lFt/Ph5aTlIjyh6Qqqytc/ghL4MaOQM7h8RPOAfrZ2RbDVNs3+l+IXHLYYLCHNa0644xAgqSirxU1gIOBlbiLdAndYX0II8IgTDII0wzCLIOwyCBc4cKu4dlNFXaHP9sWTtyR4MD5NAYg9s17mSKyvOboCQrPyOmJoPAqPSoBFN6HZSaDApjwIj0ZeEAw0AKQ1TnJabIHH6vLIPPQAK6M/SiIkW0IU27qT8eZPitTe9bPj6GSZmEW1pHZLyhh6Y3R1dDHYxFqzxOMK4/vhwnFgAZIozS6RzpKqz0eAxqnF9ScZH1kM+i7/1xvAP04Y7L9rQhtAYwt7Zvs6TSmx2iNmchBkcSIjOt7rG1iUNHKPzN5BupWHYpP4V451W06ZyFJ0F6gTvCrVCv5dke0eIM5HaA9+0OgHG/SdfBq/gtKLPcNkwIYfJxc3Dy8/AKCwqIS0jAECo2XV1ZR19I1MDQyNjGztXcmF5gV75JuhfcjmtBT2C5cJ76diLsGUSvXDGrE3EmBe4hOOWmQJOeK88ShqHxc5Zt63PibyVezb8RcH3g+IKryH9Q/gBANq3AgGhFPSt5J5aQzsDI8hQxQATqGCWM/4r7j/5kHlnfWYduf9hGnsPNPlzCtcFk0kMpDtPAssowqoz9iStiUedm6ZB84lVxKxMIpcjqZQgnM80M0HyWj06J5PlqDcxZobuk0lbmuv83aUzqnCUTrUNHOiAQSgl8gevQrQZF5h4sj4rQ8Dwl5a/xliEVJmXXEy02EKZShAC3IQR/KUNKLpHSRd6mCXOKfAgoIJlJ1/lkkK/4sQS2Vkf4JTy+BmPkmvIM1uB95FcqnWBTlH6kO3trKI3TzAK4GJoJpJobFK0ngtgpmuMsDJ6xuTMKW4eyZpPMHlQKhWxM3cGDAYTZhhckJ27QA/wa60QNCXJgBMppdD10DUqDc99jNkVEE37EeTVjgY/exq9/DeykXkpfTJwS4+z7lAGL3IgDMEWyQuIpCLvfjL0cQhzIoY5bxm4E+YE1Ad4zvyyrVVTrAkIQdiR3REyB08wfsXrl+w8UGzKI0bi/wH+Dl2jVhAOwHJKGopPgIU9F04QlCYEwEPwd/io4QPFR11EZzDAY15mIlNuN63O4gSuvz10dLDMdYzMdq7Izy/Z9kDABEZEYPFEaKEQcE2qy2uCQLuO1aZ9jlORQUlThvXPdt2JLQYQ+nx5GkASlD0h9AITPurayQKQ+evHjz4cuPup1AGrY0EUgUGoN1+DXTbVzID1qEz+Bnbx6A3AJrFxjFYNiCBWg/wQF2BrwOZmbLSOegl+CA4wfcef99OCx1J6eWH5zMwg7GZgyMBXX0URAqJXSEjUaGgQqxQfph2Cy1EGecJxxRB/pCn+5At/p+x1i7bG0JB9REf5MJA9012xqp4QbV2Nwddg4Oht3NLb2NhqIyFYpBaTsqspIhs65IVtRLvStJ1ztgrUod2LYscl0PGPOhnFh6iWR4BA3UCNma0DUCSYrIlTobr5Y52om1M/28oqhCuoLOXhmrO/e8E1QN/HYroSQb27LWzczisvfRSbQcZ5wRFdgkFlgSHhD9ChWhHs5u27MiFWCoWDOVdOGeKhZUqahfoYCyjtit6qNGaGJkWDPsxSFU6gMatNbK2hBXrFOv1ezB1MpY3TkZ+OaomFe/80ecEanr5tO+DHB1z2COtNcnCCzU/AGOjFByeZY/geQ6njv3OVyHyQLM+gyokWSlehRVSTF94DWEyrFXXGuEBorAVGEwhskefTMVImhipSJrBHOP0o67tW0FyLKuxzj0NJPPrSM3sdexZ5EHkwd0JE/6iqOTDRkFpFwRXz7KSx2BRwCbCBSTWcayAiv1XQOwRx4JirxUMiboo6yFoHCBr0tPoLWCrY3NYVFNJN4PhW9M3EPDngAloTrnZWSyfro3Ijk6S26GI5gXBUtpIrgtNYs46LbMr9nhnBMrd9xVJIYCskvWkICQugdLG2iCgeOkJZJW0rKuvZrjO17NOMPXB2uG0Yq0EWCYKlB5WaPzuIfkZV/Jaem+jsQ4UPBopGny7O+n3CQk8qLw6YmeVtL50fGV97LmeXdb0WrGOLL6wRQmqj7mQlyz46YdJFat/gkYf3XZgbcPqdeGCEXyHrvKQx9ZM9WTABtljQX68egqAu+9iazbIEeMIztTXLCkBKPSGgawR9roqGzXnNGE/YSBCytXxYtlV7FGEueLgtmyTMV535FH98G/IcalXkmsunu84y7nwPY3Oe5dgZmnU4C8fDC1BzhTW3Ykytry6a+S9b63/CTC7uMjU/BB00cFtsgkdNb4KpllmW9qHM8nTw473U1BW3ml0fJbzacKAt3iadT4y63LIUzhnPt8RayRUSHjhkTDPM0k0K36YW5sycJGSh5JPQPPSevb3tr+vmy5/rfZPL3vKNEAQ6WhogIBw8xbbEX6wp79YhCFBFUiQSiY0/LQzXJnlomivpDJorJE4I5dDwAKYKj0X8hlWmRCf4xqlmQhNW8D++CHYONV0eyyrLgXb9D4ud+k0vjwxJyQ4p9gkl7tfX5hdRYw1LH1yWZvcCsERkVNxR5gqHvBNcEM6GcAhsoAvcyRM1dau3qy5tTonrZ4qewlVTWQuEwVswwU0w206e35qUiR2MvwKbGbYSKFT+mVwS0V9pQorKzLAShNcnL+A7fn47dbzPlOTYwJnGozhW33W21WcKiRfCdazeAmA707jfw3MgvIe8+v85hj/00e/IRGcQmerxf+O25v57bIpz21Vc2KuoIjpIbafMQAHNAvr7z89/LiegkotQxpccrN7Fx4pGgo+D9BhYuPZnfkIHnPeUwEV9Ihsi+Ca+kQhaIVtlWjEQ0Bs4/rkgPgrNCfv/+ikvKAR5TtLctAzr+XVW2v+DT3d1mOVy3+rFyeG6ldJmfXLMIfHS4P7D/hTMIN4RECAzC3vLXNLUgWFpEWib+PuKY5fSZBxJKQh9T6FsX/RzjCRyc8wXoFxLeQHfUv7gLmPtStEOycyu2dCIed7MyIDnbw+WTKqV3CLtXL5axaH8esmh7w6BOf1Pg0Au712VdFys0+6toCaqTYXrxEMywyXw68jH0kPaDwg0qXfUX1TQXPladCJQtA0Cafv3g+pTL6C1N5RzsOM60H3Wq14D8z2sE/9Jdp9CiM3jlQLrUUolhyS76i/pD8QeWBhJWLqxexFk4/r/zEZCh3rneCmxkwXhbJ/79DBq2L29WYxVVs+zXiNZOO5+utFQCTtP0hFKq++q9JzU+kdhg9ujd6HIXUVP/sH6jbQ2pHUON7/3va03+2B3OmCz04ZWDW3zcw2YE53Y3tpYLuRYtioYZzx7/t/WX6IaT5Q4TEyPoiJKyB+n7A+AE99Rf+L5zIgMebGZI53DBMWu2511jfdXcj8kOBAEli68/a3fjobFxf+HSdOLpv5Cimt0FiKqqdJBsffXPtK5jeJGCZcqx5W4Qn8I5DukNRgxcuPRf/zcn2Qo82Fd3GV/zCrI98ilRrVXHVqq46o4AGCq20rW93xkPCu3w0jqgWLRZvfPuwc5Tsfm0XMKMZuefvpjg0+6dmBYUW5sce8nHrTausTE4iN0ZD7pztTeAkfNj/JyzAs0bfFhZg/wec6PdNN0Zm7FIFncUutenGOfsZ6QYtEJ84PxJE1sS7yT+elrc+55VBHZ3Zr5QW8FeMqcwqHqpcIGeXL0wfaVxNFCJXnoMQrcDYgjBJb9nQI7Ztv0auL+9PNu0akZ39gtMcTY1C7OOunt7ZYWoxzfOODi/yNd/tRs2t3WIeA6Oj1Kb+H16JVnMJnkZ+9sIPiaE45zA3G/Kcm3FeZGC0tXiSVIzYJS27WEOXGik51wcMo0sgSCOwF5PaLkyfusREi6R7JAfFxrZZkXnpBDC/mG70y+7Fkz9maLV3ej8cXj//cRitdlnmpuYmeTUthby6eePzTZXtnO2npBVkBURpBDZjQROV0UU7IW8RPV7glf+XmO2JcxGbJMp6Yb8CarlTNynTRyV5hf/HNVYRAW7/e9L2tkwyg0xTZ8FQ936VrE9OhZfDrHjVldpwifDCChFispyiq0ESYpMz70IojrDFuyjLfmSycJAs0M2apjQNXWpQS1LMrQs7htBedOapgn1LXr+9CdZU4Z2Wv38Pxzx63smlPJCPdH76V5eXe/eJ2IWJOBKK/mCXSQpBqZpntpLyTk3M5tLSo0nnB0C21Jn28eHCy7DEjNC04oUTYiUtXXivEENNdyDaFiw5GBREKig7qSnNmXF90v+4B9uKvdl/HlSCzQsS+1zTv3ryh0fFTc+5VVEcn9llHiNEnWal0dL5nKzChXM9xeNZpPKzYHKJHOt6+ISOYpQ81UU1UQBt6Ol+4TQIyxGqUYNpjW8HmF4niX9Lf4XjQJm8Wdt+BndaIZITdUhc/2AkH53u3t5kY+WwgMQMdq63SBRm9zbltXyoLf/bTJdWYhPdou+2UERGzrcjbbVLmQYmoCdHKGkWO7Yxgn6Wwv/5yHN+NE6PQ3STvo2SYNMG1k/0t8Hih4sB50koE8J+PBe66hsQ0kOx/ueG1AW3+/viy53Dfi4V+Fb7xvAmfu1twKOQ9nrtFt5QXlewK/ZpsWDLuv+HcesGgr4p8QGRyS+qTw5PLCvJ25Y/4JvLh0Zpa0ePL2wtaNuzd3nJJOYNxktaoTqTdM1tQZbOvPNLJYIcEmpNFJW/QFMi4iwVKHwMHrk2KUszVYrs+Xn7mLwI1QSIsigp1O89i1tRXfwc8Ezews/nruLFx/S6U2bCeYCAQvUbnSIcpqK6l9xXHAKj2oDy9u9npD68LcjBfQU4BOyja2O0MtKQpxs/Qu9cvqCb48BcmK54ud+zE+s/cTwf9+vgt/AljqP5xPZUczQyR2wdDCDAQhswFYgALNDxCQOJtBqbNCxlKarIstl4EMAElQB7BibonuMhR6iP+pGOaavOlvphYkEAJHTRw0b0McAQESUq1GiwwRwpTG/p8GEMvXRz/A99DM/vGK5AjqOonERZSEtL0OEPCBm98yJdsR2bsNXVTKPsh6X0fkzL+2gFhh3KyAzjPPjjxYdMtX9Z4cpgDx90/2sDPk6rMRru+IAyX4gbBdIxCxmDiKRZjP7FoqHmSxsLpJYIY7oflN+saKV1cX/p4plTVBTH8BgcwVWtnTIoEdswb118MQUs8SBcOLr5whWNB24CHqiCWeA2KEvvxvQmaZatrO1XXJlgtbkkL0ShzSdHnl+whdHY8qOti7BFzQ9nzYIdUg8yIQlGfHnjdNa8hdCSOM0CxH0L6vXe9OaaCcUsT8MWIo9NV+djsuAXbRDAlD22UUcm5LDRXxbRHQC+f21UB8AvxP3335G9W3uBuwxgDzgABsCauNkB9hKoMfvEs0DgZLVnUSvSIMc+KA98xQFvshylzqJMc8PFDm9WBEtnlqly0SUx6HwAXzzi+RQzeodr1nOJH4SiTFAuaO6fuz471M8gV9BGXuPOZumuZaKVI6AM+bJRYo3pzp21qS/s6wTLCpCQpbzzirbkYq0qeWao0BRzQZ0ryEEZ84TRjCeU/O5Jh5f8hWlgmo1Rxyv1ul5Y2yxrhctCEZ0TSJnbyJJGx+cXyfKNqrObPM03rboaKssNqZTuzxNdqQP5a1YtaEL14GxwbzDyQLpJM+klTVQPqhPVh2oVl1joZ8b1PbUTJL3XgAB4poGQIQyq+iRkAtckwcWOvhAKGJoVwEOALWbQ5biYg4Gy2Wk3i/FiF8b8Ck/kv8EaWHYFLKRIRZYuToxYmaSQcESY79OSwoUlilq+I1kEdVEpINE1JasZqIjKVlHSkUSJpG56ivAImYaUQavSjMySRMkfI0uisAne89NliFOTlQDKpXByutw51q3xNOEjPRUBFvBbV3cpyoeJECuKui2bLoaGL74UVZM1iwyx6rNjwYozj6TiVSTghHCyWzpeJAA=) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} + +/*!************************************************************************************************!*\ + !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/font/fira-code.css ***! + \************************************************************************************************/ +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} + +/*!********************************************************************************************!*\ + !*** css ../../../node_modules/css-loader/dist/cjs.js!../../graphiql-react/dist/style.css ***! + \********************************************************************************************/ +.graphiql-container *{box-sizing:border-box;font-variant-ligatures:none}.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,.graphiql-dialog,.graphiql-dialog-overlay,.graphiql-tooltip,[data-radix-popper-content-wrapper]{--color-primary: 320, 95%, 43%;--color-secondary: 242, 51%, 61%;--color-tertiary: 188, 100%, 36%;--color-info: 208, 100%, 46%;--color-success: 158, 60%, 42%;--color-warning: 36, 100%, 41%;--color-error: 13, 93%, 58%;--color-neutral: 219, 28%, 32%;--color-base: 219, 28%, 100%;--alpha-secondary: .76;--alpha-tertiary: .5;--alpha-background-heavy: .15;--alpha-background-medium: .1;--alpha-background-light: .07;--font-family: "Roboto", sans-serif;--font-family-mono: "Fira Code", monospace;--font-size-hint:.75rem;--font-size-inline-code:.8125rem;--font-size-body:.9375rem;--font-size-h4:1.125rem;--font-size-h3:1.375rem;--font-size-h2:1.8125rem;--font-weight-regular: 400;--font-weight-medium: 500;--line-height: 1.5;--px-2: 2px;--px-4: 4px;--px-6: 6px;--px-8: 8px;--px-10: 10px;--px-12: 12px;--px-16: 16px;--px-20: 20px;--px-24: 24px;--border-radius-2: 2px;--border-radius-4: 4px;--border-radius-8: 8px;--border-radius-12: 12px;--popover-box-shadow: 0px 6px 20px rgba(59, 76, 106, .13), 0px 1.34018px 4.46726px rgba(59, 76, 106, .0774939), 0px .399006px 1.33002px rgba(59, 76, 106, .0525061);--popover-border: none;--sidebar-width: 60px;--toolbar-width: 40px;--session-header-height: 51px}@media (prefers-color-scheme: dark){body:not(.graphiql-light) .graphiql-container,body:not(.graphiql-light) .CodeMirror-info,body:not(.graphiql-light) .CodeMirror-lint-tooltip,body:not(.graphiql-light) .graphiql-dialog,body:not(.graphiql-light) .graphiql-dialog-overlay,body:not(.graphiql-light) .graphiql-tooltip,body:not(.graphiql-light) [data-radix-popper-content-wrapper]{--color-primary: 338, 100%, 67%;--color-secondary: 243, 100%, 77%;--color-tertiary: 188, 100%, 44%;--color-info: 208, 100%, 72%;--color-success: 158, 100%, 42%;--color-warning: 30, 100%, 80%;--color-error: 13, 100%, 58%;--color-neutral: 219, 29%, 78%;--color-base: 219, 29%, 18%;--popover-box-shadow: none;--popover-border: 1px solid hsl(var(--color-neutral))}}body.graphiql-dark .graphiql-container,body.graphiql-dark .CodeMirror-info,body.graphiql-dark .CodeMirror-lint-tooltip,body.graphiql-dark .graphiql-dialog,body.graphiql-dark .graphiql-dialog-overlay,body.graphiql-dark .graphiql-tooltip,body.graphiql-dark [data-radix-popper-content-wrapper]{--color-primary: 338, 100%, 67%;--color-secondary: 243, 100%, 77%;--color-tertiary: 188, 100%, 44%;--color-info: 208, 100%, 72%;--color-success: 158, 100%, 42%;--color-warning: 30, 100%, 80%;--color-error: 13, 100%, 58%;--color-neutral: 219, 29%, 78%;--color-base: 219, 29%, 18%;--popover-box-shadow: none;--popover-border: 1px solid hsl(var(--color-neutral))}.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,.graphiql-dialog,.graphiql-container:is(button),.CodeMirror-info:is(button),.CodeMirror-lint-tooltip:is(button),.graphiql-dialog:is(button){color:hsla(var(--color-neutral),1);font-family:var(--font-family);font-size:var(--font-size-body);font-weight:var(----font-weight-regular);line-height:var(--line-height)}.graphiql-container input,.CodeMirror-info input,.CodeMirror-lint-tooltip input,.graphiql-dialog input{color:hsla(var(--color-neutral),1);font-family:var(--font-family);font-size:var(--font-size-caption)}.graphiql-container input::placeholder,.CodeMirror-info input::placeholder,.CodeMirror-lint-tooltip input::placeholder,.graphiql-dialog input::placeholder{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-container a,.CodeMirror-info a,.CodeMirror-lint-tooltip a,.graphiql-dialog a{color:hsl(var(--color-primary))}.graphiql-container a:focus,.CodeMirror-info a:focus,.CodeMirror-lint-tooltip a:focus,.graphiql-dialog a:focus{outline:hsl(var(--color-primary)) auto 1px}.graphiql-un-styled,button.graphiql-un-styled{all:unset;border-radius:var(--border-radius-4);cursor:pointer}:is(.graphiql-un-styled,button.graphiql-un-styled):hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}:is(.graphiql-un-styled,button.graphiql-un-styled):active{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}:is(.graphiql-un-styled,button.graphiql-un-styled):focus{outline:hsla(var(--color-neutral),var(--alpha-background-heavy)) auto 1px}.graphiql-button,button.graphiql-button{background-color:hsla(var(--color-neutral),var(--alpha-background-light));border:none;border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),1);cursor:pointer;font-size:var(--font-size-body);padding:var(--px-8) var(--px-12)}:is(.graphiql-button,button.graphiql-button):hover,:is(.graphiql-button,button.graphiql-button):active{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}:is(.graphiql-button,button.graphiql-button):focus{outline:hsla(var(--color-neutral),var(--alpha-background-heavy)) auto 1px}.graphiql-button-success:is(.graphiql-button,button.graphiql-button){background-color:hsla(var(--color-success),var(--alpha-background-heavy))}.graphiql-button-error:is(.graphiql-button,button.graphiql-button){background-color:hsla(var(--color-error),var(--alpha-background-heavy))}.graphiql-button-group{background-color:hsla(var(--color-neutral),var(--alpha-background-light));border-radius:calc(var(--border-radius-4) + var(--px-4));display:flex;padding:var(--px-4)}.graphiql-button-group>button.graphiql-button{background-color:transparent}.graphiql-button-group>button.graphiql-button:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-button-group>button.graphiql-button.active{background-color:hsl(var(--color-base));cursor:default}.graphiql-button-group>*+*{margin-left:var(--px-8)}.graphiql-dialog-overlay{position:fixed;inset:0;background-color:hsla(var(--color-neutral),var(--alpha-background-heavy));z-index:10}.graphiql-dialog{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-12);box-shadow:var(--popover-box-shadow);margin:0;max-height:80vh;max-width:80vw;overflow:auto;padding:0;width:unset;transform:translate(-50%,-50%);top:50%;left:50%;position:fixed;z-index:10}.graphiql-dialog-close>svg{color:hsla(var(--color-neutral),var(--alpha-secondary));display:block;height:var(--px-12);padding:var(--px-12);width:var(--px-12)}.graphiql-dropdown-content{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);font-size:inherit;max-width:250px;padding:var(--px-4);font-family:var(--font-family);color:hsl(var(--color-neutral));max-height:min(calc(var(--radix-dropdown-menu-content-available-height) - 10px),400px);overflow-y:scroll}.graphiql-dropdown-item{border-radius:var(--border-radius-4);font-size:inherit;margin:var(--px-4);overflow:hidden;padding:var(--px-6) var(--px-8);text-overflow:ellipsis;white-space:nowrap;outline:none;cursor:pointer;line-height:var(--line-height)}.graphiql-dropdown-item[data-selected],.graphiql-dropdown-item[data-current-nav],.graphiql-dropdown-item:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-light));color:inherit}.graphiql-dropdown-item:not(:first-child){margin-top:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) blockquote{margin-left:0;margin-right:0;padding-left:var(--px-8)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) code,:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre{border-radius:var(--border-radius-4);font-family:var(--font-family-mono);font-size:var(--font-size-inline-code)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) code{padding:var(--px-2)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre{overflow:auto;padding:var(--px-6) var(--px-8)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) pre code{background-color:initial;border-radius:0;padding:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ol,:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ul{padding-left:var(--px-16)}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ol{list-style-type:decimal}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) ul{list-style-type:disc}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation) img{border-radius:var(--border-radius-4);max-height:120px;max-width:100%}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation)>:first-child{margin-top:0}:is(.graphiql-markdown-description,.graphiql-markdown-deprecation,.CodeMirror-hint-information-description,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-description,.CodeMirror-info .info-deprecation)>:last-child{margin-bottom:0}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) a{color:hsl(var(--color-primary));text-decoration:none}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) a:hover{text-decoration:underline}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) blockquote{border-left:1.5px solid hsla(var(--color-neutral),var(--alpha-tertiary))}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) code,:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description) pre{background-color:hsla(var(--color-neutral),var(--alpha-background-light));color:hsla(var(--color-neutral),1)}:is(.graphiql-markdown-description,.CodeMirror-hint-information-description,.CodeMirror-info .info-description)>*{margin:var(--px-12) 0}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) a{color:hsl(var(--color-warning));text-decoration:underline}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) blockquote{border-left:1.5px solid hsl(var(--color-warning))}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) code,:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation) pre{background-color:hsla(var(--color-warning),var(--alpha-background-heavy))}:is(.graphiql-markdown-deprecation,.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation)>*{margin:var(--px-8) 0}.graphiql-markdown-preview>:not(:first-child){display:none}.CodeMirror-hint-information-deprecation,.CodeMirror-info .info-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--border-radius-4);color:hsl(var(--color-warning));margin-top:var(--px-12);padding:var(--px-6) var(--px-8)}.CodeMirror-hint-information-deprecation-label,.CodeMirror-info .info-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.CodeMirror-hint-information-deprecation-reason,.CodeMirror-info .info-deprecation-reason{margin-top:var(--px-6)}.graphiql-spinner{height:56px;margin:auto;margin-top:var(--px-16);width:56px}.graphiql-spinner:after{animation:rotation .8s linear 0s infinite;border:4px solid transparent;border-radius:100%;border-top:4px solid hsla(var(--color-neutral),var(--alpha-tertiary));content:"";display:inline-block;height:46px;vertical-align:middle;width:46px}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.graphiql-tooltip{background:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-4);box-shadow:var(--popover-box-shadow);color:hsl(var(--color-neutral));font-size:inherit;padding:var(--px-4) var(--px-6);font-family:var(--font-family)}.graphiql-tabs{display:flex;align-items:center;overflow-x:auto;padding:var(--px-12)}.graphiql-tabs>:not(:first-child){margin-left:var(--px-12)}.graphiql-tab{align-items:stretch;border-radius:var(--border-radius-8);color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex}.graphiql-tab>button.graphiql-tab-close{visibility:hidden}.graphiql-tab.graphiql-tab-active>button.graphiql-tab-close,.graphiql-tab:hover>button.graphiql-tab-close,.graphiql-tab:focus-within>button.graphiql-tab-close{visibility:unset}.graphiql-tab.graphiql-tab-active{background-color:hsla(var(--color-neutral),var(--alpha-background-heavy));color:hsla(var(--color-neutral),1)}button.graphiql-tab-button{padding:var(--px-4) 0 var(--px-4) var(--px-8)}button.graphiql-tab-close{align-items:center;display:flex;padding:var(--px-4) var(--px-8)}button.graphiql-tab-close>svg{height:var(--px-8);width:var(--px-8)}.graphiql-history-header{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium);display:flex;justify-content:space-between;align-items:center}.graphiql-history-header button{font-size:var(--font-size-inline-code);padding:var(--px-6) var(--px-10)}.graphiql-history-items{margin:var(--px-16) 0 0;list-style:none;padding:0}.graphiql-history-item{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;font-size:var(--font-size-inline-code);font-family:var(--font-family-mono);height:34px}.graphiql-history-item:hover{color:hsla(var(--color-neutral),1);background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-history-item:not(:first-child){margin-top:var(--px-4)}.graphiql-history-item.editable{background-color:hsla(var(--color-primary),var(--alpha-background-medium))}.graphiql-history-item.editable>input{background:transparent;border:none;flex:1;margin:0;outline:none;padding:0 var(--px-10);width:100%}.graphiql-history-item.editable>input::placeholder{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-history-item.editable>button{color:hsl(var(--color-primary));padding:0 var(--px-10)}.graphiql-history-item.editable>button:active{background-color:hsla(var(--color-primary),var(--alpha-background-heavy))}.graphiql-history-item.editable>button:focus{outline:hsl(var(--color-primary)) auto 1px}.graphiql-history-item.editable>button>svg{display:block}button.graphiql-history-item-label{flex:1;padding:var(--px-8) var(--px-10);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}button.graphiql-history-item-action{align-items:center;color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;padding:var(--px-8) var(--px-6)}button.graphiql-history-item-action:hover{color:hsla(var(--color-neutral),1)}button.graphiql-history-item-action>svg{height:14px;width:14px}.graphiql-history-item-spacer{height:var(--px-16)}.graphiql-doc-explorer-default-value{color:hsl(var(--color-success))}a.graphiql-doc-explorer-type-name{color:hsl(var(--color-warning));text-decoration:none}a.graphiql-doc-explorer-type-name:hover{text-decoration:underline}a.graphiql-doc-explorer-type-name:focus{outline:hsl(var(--color-warning)) auto 1px}.graphiql-doc-explorer-argument>*+*{margin-top:var(--px-12)}.graphiql-doc-explorer-argument-name{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-argument-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--border-radius-4);color:hsl(var(--color-warning));padding:var(--px-8)}.graphiql-doc-explorer-argument-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.graphiql-doc-explorer-deprecation{background-color:hsla(var(--color-warning),var(--alpha-background-light));border:1px solid hsl(var(--color-warning));border-radius:var(--px-4);color:hsl(var(--color-warning));padding:var(--px-8)}.graphiql-doc-explorer-deprecation-label{font-size:var(--font-size-hint);font-weight:var(--font-weight-medium)}.graphiql-doc-explorer-directive{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-section-title{align-items:center;display:flex;font-size:var(--font-size-hint);font-weight:var(--font-weight-medium);line-height:1}.graphiql-doc-explorer-section-title>svg{height:var(--px-16);margin-right:var(--px-8);width:var(--px-16)}.graphiql-doc-explorer-section-content{margin-left:var(--px-8);margin-top:var(--px-16)}.graphiql-doc-explorer-section-content>*+*{margin-top:var(--px-16)}.graphiql-doc-explorer-root-type{color:hsl(var(--color-info))}.graphiql-doc-explorer-search{color:hsla(var(--color-neutral),var(--alpha-secondary))}.graphiql-doc-explorer-search:not([data-state="idle"]){border:var(--popover-border);border-radius:var(--border-radius-4);box-shadow:var(--popover-box-shadow);color:hsla(var(--color-neutral),1)}.graphiql-doc-explorer-search:not([data-state="idle"]) .graphiql-doc-explorer-search-input{background:hsl(var(--color-base))}.graphiql-doc-explorer-search-input{align-items:center;background-color:hsla(var(--color-neutral),var(--alpha-background-light));border-radius:var(--border-radius-4);display:flex;padding:var(--px-8) var(--px-12)}.graphiql-doc-explorer-search [role=combobox]{border:none;background-color:transparent;margin-left:var(--px-4);width:100%}.graphiql-doc-explorer-search [role=combobox]:focus{outline:none}.graphiql-doc-explorer-search [role=listbox]{background-color:hsl(var(--color-base));border:none;border-bottom-left-radius:var(--border-radius-4);border-bottom-right-radius:var(--border-radius-4);border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));max-height:400px;overflow-y:auto;margin:0;font-size:var(--font-size-body);padding:var(--px-4);position:relative}.graphiql-doc-explorer-search [role=option]{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));overflow-x:hidden;padding:var(--px-8) var(--px-12);text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.graphiql-doc-explorer-search [role=option][data-headlessui-state=active]{background-color:hsla(var(--color-neutral),var(--alpha-background-light))}.graphiql-doc-explorer-search [role=option]:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-medium))}.graphiql-doc-explorer-search [role=option][data-headlessui-state=active]:hover{background-color:hsla(var(--color-neutral),var(--alpha-background-heavy))}:is(.graphiql-doc-explorer-search [role="option"])+:is(.graphiql-doc-explorer-search [role="option"]){margin-top:var(--px-4)}.graphiql-doc-explorer-search-type{color:hsl(var(--color-info))}.graphiql-doc-explorer-search-field{color:hsl(var(--color-warning))}.graphiql-doc-explorer-search-argument{color:hsl(var(--color-secondary))}.graphiql-doc-explorer-search-divider{color:hsla(var(--color-neutral),var(--alpha-secondary));font-size:var(--font-size-hint);font-weight:var(--font-weight-medium);margin-top:var(--px-8);padding:var(--px-8) var(--px-12)}.graphiql-doc-explorer-search-empty{color:hsla(var(--color-neutral),var(--alpha-secondary));padding:var(--px-8) var(--px-12)}a.graphiql-doc-explorer-field-name{color:hsl(var(--color-info));text-decoration:none}a.graphiql-doc-explorer-field-name:hover{text-decoration:underline}a.graphiql-doc-explorer-field-name:focus{outline:hsl(var(--color-info)) auto 1px}.graphiql-doc-explorer-item>:not(:first-child){margin-top:var(--px-12)}.graphiql-doc-explorer-argument-multiple{margin-left:var(--px-8)}.graphiql-doc-explorer-enum-value{color:hsl(var(--color-info))}.graphiql-doc-explorer-header{display:flex;justify-content:space-between;position:relative}.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-title{visibility:hidden}.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-back:not(:focus){color:transparent}.graphiql-doc-explorer-header-content{display:flex;flex-direction:column;min-width:0}.graphiql-doc-explorer-search{position:absolute;right:0;top:0}.graphiql-doc-explorer-search:focus-within{left:0}.graphiql-doc-explorer-search [role=combobox]{height:24px;width:4ch}.graphiql-doc-explorer-search [role=combobox]:focus{width:100%}a.graphiql-doc-explorer-back{align-items:center;color:hsla(var(--color-neutral),var(--alpha-secondary));display:flex;text-decoration:none}a.graphiql-doc-explorer-back:hover{text-decoration:underline}a.graphiql-doc-explorer-back:focus{outline:hsla(var(--color-neutral),var(--alpha-secondary)) auto 1px}a.graphiql-doc-explorer-back:focus+.graphiql-doc-explorer-title{visibility:unset}a.graphiql-doc-explorer-back>svg{height:var(--px-8);margin-right:var(--px-8);width:var(--px-8)}.graphiql-doc-explorer-title{font-weight:var(--font-weight-medium);font-size:var(--font-size-h2);overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.graphiql-doc-explorer-title:not(:first-child){font-size:var(--font-size-h3);margin-top:var(--px-8)}.graphiql-doc-explorer-content>*{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-20)}.graphiql-doc-explorer-error{background-color:hsla(var(--color-error),var(--alpha-background-heavy));border:1px solid hsl(var(--color-error));border-radius:var(--border-radius-8);color:hsl(var(--color-error));padding:var(--px-8) var(--px-12)}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:transparent}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:transparent}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:white}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:none;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none;outline:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors,.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:#ff06}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}.graphiql-container .CodeMirror{height:100%;position:absolute;width:100%}.graphiql-container .CodeMirror{font-family:var(--font-family-mono)}.graphiql-container .CodeMirror,.graphiql-container .CodeMirror-gutters{background:none;background-color:var(--editor-background, hsl(var(--color-base)))}.graphiql-container .CodeMirror-linenumber{padding:0}.graphiql-container .CodeMirror-gutters{border:none}.cm-s-graphiql{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-keyword{color:hsl(var(--color-primary))}.cm-s-graphiql .cm-def{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-punctuation{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-variable{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-atom{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-number{color:hsl(var(--color-success))}.cm-s-graphiql .cm-string{color:hsl(var(--color-warning))}.cm-s-graphiql .cm-builtin{color:hsl(var(--color-success))}.cm-s-graphiql .cm-string-2{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-attribute,.cm-s-graphiql .cm-meta{color:hsl(var(--color-tertiary))}.cm-s-graphiql .cm-property{color:hsl(var(--color-info))}.cm-s-graphiql .cm-qualifier{color:hsl(var(--color-secondary))}.cm-s-graphiql .cm-comment{color:hsla(var(--color-neutral),var(--alpha-secondary))}.cm-s-graphiql .cm-ws{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.cm-s-graphiql .cm-invalidchar{color:hsl(var(--color-error))}.cm-s-graphiql .CodeMirror-cursor{border-left:2px solid hsla(var(--color-neutral),var(--alpha-secondary))}.cm-s-graphiql .CodeMirror-linenumber{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket,.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket{color:hsl(var(--color-warning))}.graphiql-container .CodeMirror-selected,.graphiql-container .CodeMirror-focused .CodeMirror-selected{background:hsla(var(--color-neutral),var(--alpha-background-heavy))}.graphiql-container .CodeMirror-dialog{background:inherit;color:inherit;left:0;right:0;overflow:hidden;padding:var(--px-2) var(--px-6);position:absolute;z-index:6}.graphiql-container .CodeMirror-dialog-top{border-bottom:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));padding-bottom:var(--px-12);top:0}.graphiql-container .CodeMirror-dialog-bottom{border-top:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));bottom:0;padding-top:var(--px-12)}.graphiql-container .CodeMirror-search-hint{display:none}.graphiql-container .CodeMirror-dialog input{border:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));border-radius:var(--border-radius-4);padding:var(--px-4)}.graphiql-container .CodeMirror-dialog input:focus{outline:hsl(var(--color-primary)) solid 2px}.graphiql-container .cm-searching{background-color:hsla(var(--color-warning),var(--alpha-background-light));padding-bottom:1.5px;padding-top:.5px}.CodeMirror-foldmarker{color:#00f;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"▾"}.CodeMirror-foldgutter-folded:after{content:"▸"}.CodeMirror-foldgutter{width:var(--px-12)}.CodeMirror-foldmarker{background-color:hsl(var(--color-info));border-radius:var(--border-radius-4);color:hsl(var(--color-base));font-family:inherit;margin:0 var(--px-4);padding:0 var(--px-8);text-shadow:none}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{color:hsla(var(--color-neutral),var(--alpha-tertiary))}.CodeMirror-foldgutter-open:after,.CodeMirror-foldgutter-folded:after{margin:0 var(--px-2)}.graphiql-editor{height:100%;position:relative;width:100%}.graphiql-editor.hidden{left:-9999px;position:absolute;top:-9999px;visibility:hidden}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:#ffd;border:1px solid black;border-radius:4px;color:#000;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-marker{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message{padding-left:18px;background-position:top left;background-repeat:no-repeat}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.CodeMirror-lint-marker-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:right bottom;width:100%;height:100%}.CodeMirror-lint-line-error{background-color:#b74c5114}.CodeMirror-lint-line-warning{background-color:#ffd3001a}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-repeat:repeat-x;background-size:10px 3px;background-position:0 95%}.cm-s-graphiql .CodeMirror-lint-mark-error{color:hsl(var(--color-error))}.CodeMirror-lint-mark-error{background-image:linear-gradient(45deg,transparent 65%,hsl(var(--color-error)) 80%,transparent 90%),linear-gradient(135deg,transparent 5%,hsl(var(--color-error)) 15%,transparent 25%),linear-gradient(135deg,transparent 45%,hsl(var(--color-error)) 55%,transparent 65%),linear-gradient(45deg,transparent 25%,hsl(var(--color-error)) 35%,transparent 50%)}.cm-s-graphiql .CodeMirror-lint-mark-warning{color:hsl(var(--color-warning))}.CodeMirror-lint-mark-warning{background-image:linear-gradient(45deg,transparent 65%,hsl(var(--color-warning)) 80%,transparent 90%),linear-gradient(135deg,transparent 5%,hsl(var(--color-warning)) 15%,transparent 25%),linear-gradient(135deg,transparent 45%,hsl(var(--color-warning)) 55%,transparent 65%),linear-gradient(45deg,transparent 25%,hsl(var(--color-warning)) 35%,transparent 50%)}.CodeMirror-lint-tooltip{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);font-size:var(--font-size-body);font-family:var(--font-family);max-width:600px;overflow:hidden;padding:var(--px-12)}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{background-image:none;padding:0}.CodeMirror-lint-message-error{color:hsl(var(--color-error))}.CodeMirror-lint-message-warning{color:hsl(var(--color-warning))}.CodeMirror-hints{position:absolute;z-index:10;overflow:hidden;list-style:none;margin:0;padding:2px;-webkit-box-shadow:2px 3px 5px rgba(0,0,0,.2);-moz-box-shadow:2px 3px 5px rgba(0,0,0,.2);box-shadow:2px 3px 5px #0003;border-radius:3px;border:1px solid silver;background:white;font-size:90%;font-family:monospace;max-height:20em;overflow-y:auto}.CodeMirror-hint{margin:0;padding:0 4px;border-radius:2px;white-space:pre;color:#000;cursor:pointer}li.CodeMirror-hint-active{background:#08f;color:#fff}.CodeMirror-hints{background:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);display:grid;font-family:var(--font-family);font-size:var(--font-size-body);grid-template-columns:auto fit-content(300px);max-height:264px;padding:0}.CodeMirror-hint{border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));grid-column:1 / 2;margin:var(--px-4);padding:var(--px-6) var(--px-8)!important}.CodeMirror-hint:not(:first-child){margin-top:0}li.CodeMirror-hint-active{background:hsla(var(--color-primary),var(--alpha-background-medium));color:hsl(var(--color-primary))}.CodeMirror-hint-information{border-left:1px solid hsla(var(--color-neutral),var(--alpha-background-heavy));grid-column:2 / 3;grid-row:1 / 99999;max-height:264px;overflow:auto;padding:var(--px-12)}.CodeMirror-hint-information-header{display:flex;align-items:baseline}.CodeMirror-hint-information-field-name{font-size:var(--font-size-h4);font-weight:var(--font-weight-medium)}.CodeMirror-hint-information-type-name-pill{border:1px solid hsla(var(--color-neutral),var(--alpha-tertiary));border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));margin-left:var(--px-6);padding:var(--px-4)}.CodeMirror-hint-information-type-name{color:inherit;text-decoration:none}.CodeMirror-hint-information-type-name:hover{text-decoration:underline dotted}.CodeMirror-hint-information-description{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-12)}.CodeMirror-info{background-color:hsl(var(--color-base));border:var(--popover-border);border-radius:var(--border-radius-8);box-shadow:var(--popover-box-shadow);color:hsla(var(--color-neutral),1);max-height:300px;max-width:400px;opacity:0;overflow:auto;padding:var(--px-12);position:fixed;transition:opacity .15s;z-index:10}.CodeMirror-info a{color:inherit;text-decoration:none}.CodeMirror-info a:hover{text-decoration:underline dotted}.CodeMirror-info .CodeMirror-info-header{display:flex;align-items:baseline}.CodeMirror-info .CodeMirror-info-header>.type-name,.CodeMirror-info .CodeMirror-info-header>.field-name,.CodeMirror-info .CodeMirror-info-header>.arg-name,.CodeMirror-info .CodeMirror-info-header>.directive-name,.CodeMirror-info .CodeMirror-info-header>.enum-value{font-size:var(--font-size-h4);font-weight:var(--font-weight-medium)}.CodeMirror-info .type-name-pill{border:1px solid hsla(var(--color-neutral),var(--alpha-tertiary));border-radius:var(--border-radius-4);color:hsla(var(--color-neutral),var(--alpha-secondary));margin-left:var(--px-6);padding:var(--px-4)}.CodeMirror-info .info-description{color:hsla(var(--color-neutral),var(--alpha-secondary));margin-top:var(--px-12);overflow:hidden}.CodeMirror-jump-token{text-decoration:underline dotted;cursor:pointer}.auto-inserted-leaf.cm-property{animation-duration:6s;animation-name:insertionFade;border-radius:var(--border-radius-4);padding:var(--px-2)}@keyframes insertionFade{0%,to{background-color:none}15%,85%{background-color:hsla(var(--color-warning),var(--alpha-background-light))}}button.graphiql-toolbar-button{display:flex;align-items:center;justify-content:center;height:var(--toolbar-width);width:var(--toolbar-width)}button.graphiql-toolbar-button.error{background:hsla(var(--color-error),var(--alpha-background-heavy))}.graphiql-execute-button-wrapper{position:relative}button.graphiql-execute-button{background-color:hsl(var(--color-primary));border:none;border-radius:var(--border-radius-8);cursor:pointer;height:var(--toolbar-width);padding:0;width:var(--toolbar-width)}button.graphiql-execute-button:hover{background-color:hsla(var(--color-primary),.9)}button.graphiql-execute-button:active{background-color:hsla(var(--color-primary),.8)}button.graphiql-execute-button:focus{outline:hsla(var(--color-primary),.8) auto 1px}button.graphiql-execute-button>svg{color:#fff;display:block;height:var(--px-16);margin:auto;width:var(--px-16)}button.graphiql-toolbar-menu{display:block;height:var(--toolbar-width);width:var(--toolbar-width)} + +/*!*********************************************************************************************************************!*\ + !*** css ../../../node_modules/css-loader/dist/cjs.js!../../../node_modules/postcss-loader/dist/cjs.js!./style.css ***! + \*********************************************************************************************************************/ +/* Everything */ +.graphiql-container { + background-color: hsl(var(--color-base)); + display: flex; + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; +} +/* The sidebar */ +.graphiql-container .graphiql-sidebar { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: var(--px-8); + width: var(--sidebar-width); +} +.graphiql-container .graphiql-sidebar .graphiql-sidebar-section { + display: flex; + flex-direction: column; + gap: var(--px-8); +} +.graphiql-container .graphiql-sidebar button { + display: flex; + align-items: center; + justify-content: center; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + height: calc(var(--sidebar-width) - (2 * var(--px-8))); + width: calc(var(--sidebar-width) - (2 * var(--px-8))); +} +.graphiql-container .graphiql-sidebar button.active { + color: hsla(var(--color-neutral), 1); +} +.graphiql-container .graphiql-sidebar button:not(:first-child) { + margin-top: var(--px-4); +} +.graphiql-container .graphiql-sidebar button > svg { + height: var(--px-20); + width: var(--px-20); +} +/* The main content, i.e. everything except the sidebar */ +.graphiql-container .graphiql-main { + display: flex; + flex: 1; + min-width: 0; +} +/* The current session and tabs */ +.graphiql-container .graphiql-sessions { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + /* Adding the 8px of padding to the inner border radius of the query editor */ + border-radius: calc(var(--border-radius-12) + var(--px-8)); + display: flex; + flex-direction: column; + flex: 1; + max-height: 100%; + margin: var(--px-16); + margin-left: 0; + min-width: 0; +} +/* The session header containing tabs and the logo */ +.graphiql-container .graphiql-session-header { + align-items: center; + display: flex; + justify-content: space-between; + height: var(--session-header-height); +} +/* The button to add a new tab */ +button.graphiql-tab-add { + height: 100%; + padding: var(--px-4); +} +button.graphiql-tab-add > svg { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: block; + height: var(--px-16); + width: var(--px-16); +} +/* The right-hand-side of the session header */ +.graphiql-container .graphiql-session-header-right { + align-items: center; + display: flex; +} +/* The GraphiQL logo */ +.graphiql-container .graphiql-logo { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); + padding: var(--px-12) var(--px-16); +} +/* Undo default link styling for the default GraphiQL logo link */ +.graphiql-container .graphiql-logo .graphiql-logo-link { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + text-decoration: none; +} +/* The editor of the session */ +.graphiql-container .graphiql-session { + display: flex; + flex: 1; + padding: 0 var(--px-8) var(--px-8); +} +/* All editors (query, variable, headers) */ +.graphiql-container .graphiql-editors { + background-color: hsl(var(--color-base)); + border-radius: calc(var(--border-radius-12)); + box-shadow: var(--popover-box-shadow); + display: flex; + flex: 1; + flex-direction: column; +} +.graphiql-container .graphiql-editors.full-height { + margin-top: calc(var(--px-8) - var(--session-header-height)); +} +/* The query editor and the toolbar */ +.graphiql-container .graphiql-query-editor { + border-bottom: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-16); + column-gap: var(--px-16); + display: flex; + width: 100%; +} +/* The vertical toolbar next to the query editor */ +.graphiql-container .graphiql-toolbar { + width: var(--toolbar-width); +} +.graphiql-container .graphiql-toolbar > * + * { + margin-top: var(--px-8); +} +/* The toolbar icons */ +.graphiql-toolbar-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: calc(var(--toolbar-width) - (var(--px-8) * 2)); + width: calc(var(--toolbar-width) - (var(--px-8) * 2)); +} +/* The tab bar for editor tools */ +.graphiql-container .graphiql-editor-tools { + cursor: row-resize; + display: flex; + width: 100%; + column-gap: var(--px-8); + padding: var(--px-8); +} +.graphiql-container .graphiql-editor-tools button { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-container .graphiql-editor-tools button.active { + color: hsla(var(--color-neutral), 1); +} +/* The tab buttons to switch between editor tools */ +.graphiql-container + .graphiql-editor-tools + > button:not(.graphiql-toggle-editor-tools) { + padding: var(--px-8) var(--px-12); +} +.graphiql-container .graphiql-editor-tools .graphiql-toggle-editor-tools { + margin-left: auto; +} +/* An editor tool, e.g. variable or header editor */ +.graphiql-container .graphiql-editor-tool { + flex: 1; + padding: var(--px-16); +} +/** + * The way CodeMirror editors are styled they overflow their containing + * element. For some OS-browser-combinations this might cause overlap issues, + * setting the position of this to `relative` makes sure this element will + * always be on top of any editors. + */ +.graphiql-container .graphiql-toolbar, +.graphiql-container .graphiql-editor-tools, +.graphiql-container .graphiql-editor-tool { + position: relative; +} +/* The response view */ +.graphiql-container .graphiql-response { + --editor-background: transparent; + display: flex; + width: 100%; + flex-direction: column; +} +/* The results editor wrapping container */ +.graphiql-container .graphiql-response .result-window { + position: relative; + flex: 1; +} +/* The footer below the response view */ +.graphiql-container .graphiql-footer { + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); +} +/* The plugin container */ +.graphiql-container .graphiql-plugin { + border-left: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + flex: 1; + overflow-y: auto; + padding: var(--px-16); +} +/* Generic drag bar for horizontal resizing */ +.graphiql-horizontal-drag-bar { + width: var(--px-12); + cursor: col-resize; +} +.graphiql-horizontal-drag-bar:hover::after { + border: var(--px-2) solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-radius: var(--border-radius-2); + content: ''; + display: block; + height: 25%; + margin: 0 auto; + position: relative; + /* (100% - 25%) / 2 = 37.5% */ + top: 37.5%; + width: 0; +} +.graphiql-container .graphiql-chevron-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: var(--px-12); + margin: var(--px-12); + width: var(--px-12); +} +/* Generic spin animation */ +.graphiql-spin { + animation: spin 0.8s linear 0s infinite; +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} +/* The header of the settings dialog */ +.graphiql-dialog .graphiql-dialog-header { + align-items: center; + display: flex; + justify-content: space-between; + padding: var(--px-24); +} +/* The title of the settings dialog */ +.graphiql-dialog .graphiql-dialog-title { + font-size: var(--font-size-h3); + font-weight: var(--font-weight-medium); + margin: 0; +} +/* A section inside the settings dialog */ +.graphiql-dialog .graphiql-dialog-section { + align-items: center; + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + display: flex; + justify-content: space-between; + padding: var(--px-24); +} +.graphiql-dialog .graphiql-dialog-section > :not(:first-child) { + margin-left: var(--px-24); +} +/* The section title in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-title { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +/* The section caption in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-caption { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-dialog .graphiql-warning-text { + color: hsl(var(--color-warning)); + font-weight: var(--font-weight-medium); +} +.graphiql-dialog .graphiql-table { + border-collapse: collapse; + width: 100%; +} +.graphiql-dialog .graphiql-table :is(th, td) { + border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-8) var(--px-12); +} +/* A single key the short-key dialog */ +.graphiql-dialog .graphiql-key { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); + border-radius: var(--border-radius-4); + padding: var(--px-4); +} +/* Avoid showing native tooltips for icons with titles */ +.graphiql-container svg { + pointer-events: none; +} + + +/*# sourceMappingURL=graphiql.css.map*/ \ No newline at end of file diff --git a/src/Laravel/public/graphiql/graphiql.min.js b/src/Laravel/public/graphiql/graphiql.min.js new file mode 100644 index 00000000000..39edad7af18 --- /dev/null +++ b/src/Laravel/public/graphiql/graphiql.min.js @@ -0,0 +1,83643 @@ +/******/ (function() { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ "../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.browser.esm.js": +/*!**************************************************************************************!*\ + !*** ../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.browser.esm.js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _memoize = _interopRequireDefault(__webpack_require__(/*! @emotion/memoize */ "../../../node_modules/@emotion/memoize/dist/memoize.browser.esm.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +var reactPropsRegex = /^((children|dangerouslySetInnerHTML|key|ref|autoFocus|defaultValue|defaultChecked|innerHTML|suppressContentEditableWarning|suppressHydrationWarning|valueLink|accept|acceptCharset|accessKey|action|allow|allowUserMedia|allowPaymentRequest|allowFullScreen|allowTransparency|alt|async|autoComplete|autoPlay|capture|cellPadding|cellSpacing|challenge|charSet|checked|cite|classID|className|cols|colSpan|content|contentEditable|contextMenu|controls|controlsList|coords|crossOrigin|data|dateTime|decoding|default|defer|dir|disabled|disablePictureInPicture|download|draggable|encType|form|formAction|formEncType|formMethod|formNoValidate|formTarget|frameBorder|headers|height|hidden|high|href|hrefLang|htmlFor|httpEquiv|id|inputMode|integrity|is|keyParams|keyType|kind|label|lang|list|loading|loop|low|marginHeight|marginWidth|max|maxLength|media|mediaGroup|method|min|minLength|multiple|muted|name|nonce|noValidate|open|optimum|pattern|placeholder|playsInline|poster|preload|profile|radioGroup|readOnly|referrerPolicy|rel|required|reversed|role|rows|rowSpan|sandbox|scope|scoped|scrolling|seamless|selected|shape|size|sizes|slot|span|spellCheck|src|srcDoc|srcLang|srcSet|start|step|style|summary|tabIndex|target|title|type|useMap|value|width|wmode|wrap|about|datatype|inlist|prefix|property|resource|typeof|vocab|autoCapitalize|autoCorrect|autoSave|color|inert|itemProp|itemScope|itemType|itemID|itemRef|on|results|security|unselectable|accentHeight|accumulate|additive|alignmentBaseline|allowReorder|alphabetic|amplitude|arabicForm|ascent|attributeName|attributeType|autoReverse|azimuth|baseFrequency|baselineShift|baseProfile|bbox|begin|bias|by|calcMode|capHeight|clip|clipPathUnits|clipPath|clipRule|colorInterpolation|colorInterpolationFilters|colorProfile|colorRendering|contentScriptType|contentStyleType|cursor|cx|cy|d|decelerate|descent|diffuseConstant|direction|display|divisor|dominantBaseline|dur|dx|dy|edgeMode|elevation|enableBackground|end|exponent|externalResourcesRequired|fill|fillOpacity|fillRule|filter|filterRes|filterUnits|floodColor|floodOpacity|focusable|fontFamily|fontSize|fontSizeAdjust|fontStretch|fontStyle|fontVariant|fontWeight|format|from|fr|fx|fy|g1|g2|glyphName|glyphOrientationHorizontal|glyphOrientationVertical|glyphRef|gradientTransform|gradientUnits|hanging|horizAdvX|horizOriginX|ideographic|imageRendering|in|in2|intercept|k|k1|k2|k3|k4|kernelMatrix|kernelUnitLength|kerning|keyPoints|keySplines|keyTimes|lengthAdjust|letterSpacing|lightingColor|limitingConeAngle|local|markerEnd|markerMid|markerStart|markerHeight|markerUnits|markerWidth|mask|maskContentUnits|maskUnits|mathematical|mode|numOctaves|offset|opacity|operator|order|orient|orientation|origin|overflow|overlinePosition|overlineThickness|panose1|paintOrder|pathLength|patternContentUnits|patternTransform|patternUnits|pointerEvents|points|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|r|radius|refX|refY|renderingIntent|repeatCount|repeatDur|requiredExtensions|requiredFeatures|restart|result|rotate|rx|ry|scale|seed|shapeRendering|slope|spacing|specularConstant|specularExponent|speed|spreadMethod|startOffset|stdDeviation|stemh|stemv|stitchTiles|stopColor|stopOpacity|strikethroughPosition|strikethroughThickness|string|stroke|strokeDasharray|strokeDashoffset|strokeLinecap|strokeLinejoin|strokeMiterlimit|strokeOpacity|strokeWidth|surfaceScale|systemLanguage|tableValues|targetX|targetY|textAnchor|textDecoration|textRendering|textLength|to|transform|u1|u2|underlinePosition|underlineThickness|unicode|unicodeBidi|unicodeRange|unitsPerEm|vAlphabetic|vHanging|vIdeographic|vMathematical|values|vectorEffect|version|vertAdvY|vertOriginX|vertOriginY|viewBox|viewTarget|visibility|widths|wordSpacing|writingMode|x|xHeight|x1|x2|xChannelSelector|xlinkActuate|xlinkArcrole|xlinkHref|xlinkRole|xlinkShow|xlinkTitle|xlinkType|xmlBase|xmlns|xmlnsXlink|xmlLang|xmlSpace|y|y1|y2|yChannelSelector|z|zoomAndPan|for|class|autofocus)|(([Dd][Aa][Tt][Aa]|[Aa][Rr][Ii][Aa]|x)-.*))$/; // https://esbench.com/bench/5bfee68a4cd7e6009ef61d23 + +var index = (0, _memoize.default)(function (prop) { + return reactPropsRegex.test(prop) || prop.charCodeAt(0) === 111 + /* o */ && prop.charCodeAt(1) === 110 + /* n */ && prop.charCodeAt(2) < 91; +} +/* Z+1 */); +var _default = index; +exports["default"] = _default; + +/***/ }), + +/***/ "../../../node_modules/@emotion/memoize/dist/memoize.browser.esm.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@emotion/memoize/dist/memoize.browser.esm.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +function memoize(fn) { + var cache = {}; + return function (arg) { + if (cache[arg] === undefined) cache[arg] = fn(arg); + return cache[arg]; + }; +} +var _default = memoize; +exports["default"] = _default; + +/***/ }), + +/***/ "../../../node_modules/@floating-ui/core/dist/floating-ui.core.esm.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@floating-ui/core/dist/floating-ui.core.esm.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.computePosition = exports.autoPlacement = exports.arrow = void 0; +exports.detectOverflow = detectOverflow; +exports.offset = exports.limitShift = exports.inline = exports.hide = exports.flip = void 0; +exports.rectToClientRect = rectToClientRect; +exports.size = exports.shift = void 0; +function getAlignment(placement) { + return placement.split('-')[1]; +} +function getLengthFromAxis(axis) { + return axis === 'y' ? 'height' : 'width'; +} +function getSide(placement) { + return placement.split('-')[0]; +} +function getMainAxisFromPlacement(placement) { + return ['top', 'bottom'].includes(getSide(placement)) ? 'x' : 'y'; +} +function computeCoordsFromPlacement(_ref, placement, rtl) { + let { + reference, + floating + } = _ref; + const commonX = reference.x + reference.width / 2 - floating.width / 2; + const commonY = reference.y + reference.height / 2 - floating.height / 2; + const mainAxis = getMainAxisFromPlacement(placement); + const length = getLengthFromAxis(mainAxis); + const commonAlign = reference[length] / 2 - floating[length] / 2; + const side = getSide(placement); + const isVertical = mainAxis === 'x'; + let coords; + switch (side) { + case 'top': + coords = { + x: commonX, + y: reference.y - floating.height + }; + break; + case 'bottom': + coords = { + x: commonX, + y: reference.y + reference.height + }; + break; + case 'right': + coords = { + x: reference.x + reference.width, + y: commonY + }; + break; + case 'left': + coords = { + x: reference.x - floating.width, + y: commonY + }; + break; + default: + coords = { + x: reference.x, + y: reference.y + }; + } + switch (getAlignment(placement)) { + case 'start': + coords[mainAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); + break; + case 'end': + coords[mainAxis] += commonAlign * (rtl && isVertical ? -1 : 1); + break; + } + return coords; +} + +/** + * Computes the `x` and `y` coordinates that will place the floating element + * next to a reference element when it is given a certain positioning strategy. + * + * This export does not have any `platform` interface logic. You will need to + * write one for the platform you are using Floating UI with. + */ +const computePosition = async (reference, floating, config) => { + const { + placement = 'bottom', + strategy = 'absolute', + middleware = [], + platform + } = config; + const validMiddleware = middleware.filter(Boolean); + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating)); + let rects = await platform.getElementRects({ + reference, + floating, + strategy + }); + let { + x, + y + } = computeCoordsFromPlacement(rects, placement, rtl); + let statefulPlacement = placement; + let middlewareData = {}; + let resetCount = 0; + for (let i = 0; i < validMiddleware.length; i++) { + const { + name, + fn + } = validMiddleware[i]; + const { + x: nextX, + y: nextY, + data, + reset + } = await fn({ + x, + y, + initialPlacement: placement, + placement: statefulPlacement, + strategy, + middlewareData, + rects, + platform, + elements: { + reference, + floating + } + }); + x = nextX != null ? nextX : x; + y = nextY != null ? nextY : y; + middlewareData = { + ...middlewareData, + [name]: { + ...middlewareData[name], + ...data + } + }; + if (reset && resetCount <= 50) { + resetCount++; + if (typeof reset === 'object') { + if (reset.placement) { + statefulPlacement = reset.placement; + } + if (reset.rects) { + rects = reset.rects === true ? await platform.getElementRects({ + reference, + floating, + strategy + }) : reset.rects; + } + ({ + x, + y + } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); + } + i = -1; + continue; + } + } + return { + x, + y, + placement: statefulPlacement, + strategy, + middlewareData + }; +}; +exports.computePosition = computePosition; +function evaluate(value, param) { + return typeof value === 'function' ? value(param) : value; +} +function expandPaddingObject(padding) { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + ...padding + }; +} +function getSideObjectFromPadding(padding) { + return typeof padding !== 'number' ? expandPaddingObject(padding) : { + top: padding, + right: padding, + bottom: padding, + left: padding + }; +} +function rectToClientRect(rect) { + return { + ...rect, + top: rect.y, + left: rect.x, + right: rect.x + rect.width, + bottom: rect.y + rect.height + }; +} + +/** + * Resolves with an object of overflow side offsets that determine how much the + * element is overflowing a given clipping boundary on each side. + * - positive = overflowing the boundary by that number of pixels + * - negative = how many pixels left before it will overflow + * - 0 = lies flush with the boundary + * @see https://floating-ui.com/docs/detectOverflow + */ +async function detectOverflow(state, options) { + var _await$platform$isEle; + if (options === void 0) { + options = {}; + } + const { + x, + y, + platform, + rects, + elements, + strategy + } = state; + const { + boundary = 'clippingAncestors', + rootBoundary = 'viewport', + elementContext = 'floating', + altBoundary = false, + padding = 0 + } = evaluate(options, state); + const paddingObject = getSideObjectFromPadding(padding); + const altContext = elementContext === 'floating' ? 'reference' : 'floating'; + const element = elements[altBoundary ? altContext : elementContext]; + const clippingClientRect = rectToClientRect(await platform.getClippingRect({ + element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))), + boundary, + rootBoundary, + strategy + })); + const rect = elementContext === 'floating' ? { + ...rects.floating, + x, + y + } : rects.reference; + const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating)); + const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || { + x: 1, + y: 1 + } : { + x: 1, + y: 1 + }; + const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ + rect, + offsetParent, + strategy + }) : rect); + return { + top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, + bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, + left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, + right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x + }; +} +const min = Math.min; +const max = Math.max; +function within(min$1, value, max$1) { + return max(min$1, min(value, max$1)); +} + +/** + * Provides data to position an inner element of the floating element so that it + * appears centered to the reference element. + * @see https://floating-ui.com/docs/arrow + */ +const arrow = options => ({ + name: 'arrow', + options, + async fn(state) { + const { + x, + y, + placement, + rects, + platform, + elements + } = state; + // Since `element` is required, we don't Partial<> the type. + const { + element, + padding = 0 + } = evaluate(options, state) || {}; + if (element == null) { + return {}; + } + const paddingObject = getSideObjectFromPadding(padding); + const coords = { + x, + y + }; + const axis = getMainAxisFromPlacement(placement); + const length = getLengthFromAxis(axis); + const arrowDimensions = await platform.getDimensions(element); + const isYAxis = axis === 'y'; + const minProp = isYAxis ? 'top' : 'left'; + const maxProp = isYAxis ? 'bottom' : 'right'; + const clientProp = isYAxis ? 'clientHeight' : 'clientWidth'; + const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; + const startDiff = coords[axis] - rects.reference[axis]; + const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element)); + let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; + + // DOM platform can return `window` as the `offsetParent`. + if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) { + clientSize = elements.floating[clientProp] || rects.floating[length]; + } + const centerToReference = endDiff / 2 - startDiff / 2; + + // If the padding is large enough that it causes the arrow to no longer be + // centered, modify the padding so that it is centered. + const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; + const minPadding = min(paddingObject[minProp], largestPossiblePadding); + const maxPadding = min(paddingObject[maxProp], largestPossiblePadding); + + // Make sure the arrow doesn't overflow the floating element if the center + // point is outside the floating element's bounds. + const min$1 = minPadding; + const max = clientSize - arrowDimensions[length] - maxPadding; + const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; + const offset = within(min$1, center, max); + + // If the reference is small enough that the arrow's padding causes it to + // to point to nothing for an aligned placement, adjust the offset of the + // floating element itself. This stops `shift()` from taking action, but can + // be worked around by calling it again after the `arrow()` if desired. + const shouldAddOffset = getAlignment(placement) != null && center != offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; + const alignmentOffset = shouldAddOffset ? center < min$1 ? min$1 - center : max - center : 0; + return { + [axis]: coords[axis] - alignmentOffset, + data: { + [axis]: offset, + centerOffset: center - offset + } + }; + } +}); +exports.arrow = arrow; +const sides = ['top', 'right', 'bottom', 'left']; +const allPlacements = /*#__PURE__*/sides.reduce((acc, side) => acc.concat(side, side + "-start", side + "-end"), []); +const oppositeSideMap = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' +}; +function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, side => oppositeSideMap[side]); +} +function getAlignmentSides(placement, rects, rtl) { + if (rtl === void 0) { + rtl = false; + } + const alignment = getAlignment(placement); + const mainAxis = getMainAxisFromPlacement(placement); + const length = getLengthFromAxis(mainAxis); + let mainAlignmentSide = mainAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top'; + if (rects.reference[length] > rects.floating[length]) { + mainAlignmentSide = getOppositePlacement(mainAlignmentSide); + } + return { + main: mainAlignmentSide, + cross: getOppositePlacement(mainAlignmentSide) + }; +} +const oppositeAlignmentMap = { + start: 'end', + end: 'start' +}; +function getOppositeAlignmentPlacement(placement) { + return placement.replace(/start|end/g, alignment => oppositeAlignmentMap[alignment]); +} +function getPlacementList(alignment, autoAlignment, allowedPlacements) { + const allowedPlacementsSortedByAlignment = alignment ? [...allowedPlacements.filter(placement => getAlignment(placement) === alignment), ...allowedPlacements.filter(placement => getAlignment(placement) !== alignment)] : allowedPlacements.filter(placement => getSide(placement) === placement); + return allowedPlacementsSortedByAlignment.filter(placement => { + if (alignment) { + return getAlignment(placement) === alignment || (autoAlignment ? getOppositeAlignmentPlacement(placement) !== placement : false); + } + return true; + }); +} +/** + * Optimizes the visibility of the floating element by choosing the placement + * that has the most space available automatically, without needing to specify a + * preferred placement. Alternative to `flip`. + * @see https://floating-ui.com/docs/autoPlacement + */ +const autoPlacement = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'autoPlacement', + options, + async fn(state) { + var _middlewareData$autoP, _middlewareData$autoP2, _placementsThatFitOnE; + const { + rects, + middlewareData, + placement, + platform, + elements + } = state; + const { + crossAxis = false, + alignment, + allowedPlacements = allPlacements, + autoAlignment = true, + ...detectOverflowOptions + } = evaluate(options, state); + const placements = alignment !== undefined || allowedPlacements === allPlacements ? getPlacementList(alignment || null, autoAlignment, allowedPlacements) : allowedPlacements; + const overflow = await detectOverflow(state, detectOverflowOptions); + const currentIndex = ((_middlewareData$autoP = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP.index) || 0; + const currentPlacement = placements[currentIndex]; + if (currentPlacement == null) { + return {}; + } + const { + main, + cross + } = getAlignmentSides(currentPlacement, rects, await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))); + + // Make `computeCoords` start from the right place. + if (placement !== currentPlacement) { + return { + reset: { + placement: placements[0] + } + }; + } + const currentOverflows = [overflow[getSide(currentPlacement)], overflow[main], overflow[cross]]; + const allOverflows = [...(((_middlewareData$autoP2 = middlewareData.autoPlacement) == null ? void 0 : _middlewareData$autoP2.overflows) || []), { + placement: currentPlacement, + overflows: currentOverflows + }]; + const nextPlacement = placements[currentIndex + 1]; + + // There are more placements to check. + if (nextPlacement) { + return { + data: { + index: currentIndex + 1, + overflows: allOverflows + }, + reset: { + placement: nextPlacement + } + }; + } + const placementsSortedByMostSpace = allOverflows.map(d => { + const alignment = getAlignment(d.placement); + return [d.placement, alignment && crossAxis ? + // Check along the mainAxis and main crossAxis side. + d.overflows.slice(0, 2).reduce((acc, v) => acc + v, 0) : + // Check only the mainAxis. + d.overflows[0], d.overflows]; + }).sort((a, b) => a[1] - b[1]); + const placementsThatFitOnEachSide = placementsSortedByMostSpace.filter(d => d[2].slice(0, + // Aligned placements should not check their opposite crossAxis + // side. + getAlignment(d[0]) ? 2 : 3).every(v => v <= 0)); + const resetPlacement = ((_placementsThatFitOnE = placementsThatFitOnEachSide[0]) == null ? void 0 : _placementsThatFitOnE[0]) || placementsSortedByMostSpace[0][0]; + if (resetPlacement !== placement) { + return { + data: { + index: currentIndex + 1, + overflows: allOverflows + }, + reset: { + placement: resetPlacement + } + }; + } + return {}; + } + }; +}; +exports.autoPlacement = autoPlacement; +function getExpandedPlacements(placement) { + const oppositePlacement = getOppositePlacement(placement); + return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; +} +function getSideList(side, isStart, rtl) { + const lr = ['left', 'right']; + const rl = ['right', 'left']; + const tb = ['top', 'bottom']; + const bt = ['bottom', 'top']; + switch (side) { + case 'top': + case 'bottom': + if (rtl) return isStart ? rl : lr; + return isStart ? lr : rl; + case 'left': + case 'right': + return isStart ? tb : bt; + default: + return []; + } +} +function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { + const alignment = getAlignment(placement); + let list = getSideList(getSide(placement), direction === 'start', rtl); + if (alignment) { + list = list.map(side => side + "-" + alignment); + if (flipAlignment) { + list = list.concat(list.map(getOppositeAlignmentPlacement)); + } + } + return list; +} + +/** + * Optimizes the visibility of the floating element by flipping the `placement` + * in order to keep it in view when the preferred placement(s) will overflow the + * clipping boundary. Alternative to `autoPlacement`. + * @see https://floating-ui.com/docs/flip + */ +const flip = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'flip', + options, + async fn(state) { + var _middlewareData$flip; + const { + placement, + middlewareData, + rects, + initialPlacement, + platform, + elements + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = true, + fallbackPlacements: specifiedFallbackPlacements, + fallbackStrategy = 'bestFit', + fallbackAxisSideDirection = 'none', + flipAlignment = true, + ...detectOverflowOptions + } = evaluate(options, state); + const side = getSide(placement); + const isBasePlacement = getSide(initialPlacement) === initialPlacement; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); + if (!specifiedFallbackPlacements && fallbackAxisSideDirection !== 'none') { + fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); + } + const placements = [initialPlacement, ...fallbackPlacements]; + const overflow = await detectOverflow(state, detectOverflowOptions); + const overflows = []; + let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; + if (checkMainAxis) { + overflows.push(overflow[side]); + } + if (checkCrossAxis) { + const { + main, + cross + } = getAlignmentSides(placement, rects, rtl); + overflows.push(overflow[main], overflow[cross]); + } + overflowsData = [...overflowsData, { + placement, + overflows + }]; + + // One or more sides is overflowing. + if (!overflows.every(side => side <= 0)) { + var _middlewareData$flip2, _overflowsData$filter; + const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; + const nextPlacement = placements[nextIndex]; + if (nextPlacement) { + // Try next placement and re-run the lifecycle. + return { + data: { + index: nextIndex, + overflows: overflowsData + }, + reset: { + placement: nextPlacement + } + }; + } + + // First, find the candidates that fit on the mainAxis side of overflow, + // then find the placement that fits the best on the main crossAxis side. + let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; + + // Otherwise fallback. + if (!resetPlacement) { + switch (fallbackStrategy) { + case 'bestFit': + { + var _overflowsData$map$so; + const placement = (_overflowsData$map$so = overflowsData.map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$map$so[0]; + if (placement) { + resetPlacement = placement; + } + break; + } + case 'initialPlacement': + resetPlacement = initialPlacement; + break; + } + } + if (placement !== resetPlacement) { + return { + reset: { + placement: resetPlacement + } + }; + } + } + return {}; + } + }; +}; +exports.flip = flip; +function getSideOffsets(overflow, rect) { + return { + top: overflow.top - rect.height, + right: overflow.right - rect.width, + bottom: overflow.bottom - rect.height, + left: overflow.left - rect.width + }; +} +function isAnySideFullyClipped(overflow) { + return sides.some(side => overflow[side] >= 0); +} +/** + * Provides data to hide the floating element in applicable situations, such as + * when it is not in the same clipping context as the reference element. + * @see https://floating-ui.com/docs/hide + */ +const hide = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'hide', + options, + async fn(state) { + const { + rects + } = state; + const { + strategy = 'referenceHidden', + ...detectOverflowOptions + } = evaluate(options, state); + switch (strategy) { + case 'referenceHidden': + { + const overflow = await detectOverflow(state, { + ...detectOverflowOptions, + elementContext: 'reference' + }); + const offsets = getSideOffsets(overflow, rects.reference); + return { + data: { + referenceHiddenOffsets: offsets, + referenceHidden: isAnySideFullyClipped(offsets) + } + }; + } + case 'escaped': + { + const overflow = await detectOverflow(state, { + ...detectOverflowOptions, + altBoundary: true + }); + const offsets = getSideOffsets(overflow, rects.floating); + return { + data: { + escapedOffsets: offsets, + escaped: isAnySideFullyClipped(offsets) + } + }; + } + default: + { + return {}; + } + } + } + }; +}; +exports.hide = hide; +function getBoundingRect(rects) { + const minX = min(...rects.map(rect => rect.left)); + const minY = min(...rects.map(rect => rect.top)); + const maxX = max(...rects.map(rect => rect.right)); + const maxY = max(...rects.map(rect => rect.bottom)); + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + }; +} +function getRectsByLine(rects) { + const sortedRects = rects.slice().sort((a, b) => a.y - b.y); + const groups = []; + let prevRect = null; + for (let i = 0; i < sortedRects.length; i++) { + const rect = sortedRects[i]; + if (!prevRect || rect.y - prevRect.y > prevRect.height / 2) { + groups.push([rect]); + } else { + groups[groups.length - 1].push(rect); + } + prevRect = rect; + } + return groups.map(rect => rectToClientRect(getBoundingRect(rect))); +} +/** + * Provides improved positioning for inline reference elements that can span + * over multiple lines, such as hyperlinks or range selections. + * @see https://floating-ui.com/docs/inline + */ +const inline = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'inline', + options, + async fn(state) { + const { + placement, + elements, + rects, + platform, + strategy + } = state; + // A MouseEvent's client{X,Y} coords can be up to 2 pixels off a + // ClientRect's bounds, despite the event listener being triggered. A + // padding of 2 seems to handle this issue. + const { + padding = 2, + x, + y + } = evaluate(options, state); + const nativeClientRects = Array.from((await (platform.getClientRects == null ? void 0 : platform.getClientRects(elements.reference))) || []); + const clientRects = getRectsByLine(nativeClientRects); + const fallback = rectToClientRect(getBoundingRect(nativeClientRects)); + const paddingObject = getSideObjectFromPadding(padding); + function getBoundingClientRect() { + // There are two rects and they are disjoined. + if (clientRects.length === 2 && clientRects[0].left > clientRects[1].right && x != null && y != null) { + // Find the first rect in which the point is fully inside. + return clientRects.find(rect => x > rect.left - paddingObject.left && x < rect.right + paddingObject.right && y > rect.top - paddingObject.top && y < rect.bottom + paddingObject.bottom) || fallback; + } + + // There are 2 or more connected rects. + if (clientRects.length >= 2) { + if (getMainAxisFromPlacement(placement) === 'x') { + const firstRect = clientRects[0]; + const lastRect = clientRects[clientRects.length - 1]; + const isTop = getSide(placement) === 'top'; + const top = firstRect.top; + const bottom = lastRect.bottom; + const left = isTop ? firstRect.left : lastRect.left; + const right = isTop ? firstRect.right : lastRect.right; + const width = right - left; + const height = bottom - top; + return { + top, + bottom, + left, + right, + width, + height, + x: left, + y: top + }; + } + const isLeftSide = getSide(placement) === 'left'; + const maxRight = max(...clientRects.map(rect => rect.right)); + const minLeft = min(...clientRects.map(rect => rect.left)); + const measureRects = clientRects.filter(rect => isLeftSide ? rect.left === minLeft : rect.right === maxRight); + const top = measureRects[0].top; + const bottom = measureRects[measureRects.length - 1].bottom; + const left = minLeft; + const right = maxRight; + const width = right - left; + const height = bottom - top; + return { + top, + bottom, + left, + right, + width, + height, + x: left, + y: top + }; + } + return fallback; + } + const resetRects = await platform.getElementRects({ + reference: { + getBoundingClientRect + }, + floating: elements.floating, + strategy + }); + if (rects.reference.x !== resetRects.reference.x || rects.reference.y !== resetRects.reference.y || rects.reference.width !== resetRects.reference.width || rects.reference.height !== resetRects.reference.height) { + return { + reset: { + rects: resetRects + } + }; + } + return {}; + } + }; +}; +exports.inline = inline; +async function convertValueToCoords(state, options) { + const { + placement, + platform, + elements + } = state; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const side = getSide(placement); + const alignment = getAlignment(placement); + const isVertical = getMainAxisFromPlacement(placement) === 'x'; + const mainAxisMulti = ['left', 'top'].includes(side) ? -1 : 1; + const crossAxisMulti = rtl && isVertical ? -1 : 1; + const rawValue = evaluate(options, state); + + // eslint-disable-next-line prefer-const + let { + mainAxis, + crossAxis, + alignmentAxis + } = typeof rawValue === 'number' ? { + mainAxis: rawValue, + crossAxis: 0, + alignmentAxis: null + } : { + mainAxis: 0, + crossAxis: 0, + alignmentAxis: null, + ...rawValue + }; + if (alignment && typeof alignmentAxis === 'number') { + crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis; + } + return isVertical ? { + x: crossAxis * crossAxisMulti, + y: mainAxis * mainAxisMulti + } : { + x: mainAxis * mainAxisMulti, + y: crossAxis * crossAxisMulti + }; +} + +/** + * Modifies the placement by translating the floating element along the + * specified axes. + * A number (shorthand for `mainAxis` or distance), or an axes configuration + * object may be passed. + * @see https://floating-ui.com/docs/offset + */ +const offset = function (options) { + if (options === void 0) { + options = 0; + } + return { + name: 'offset', + options, + async fn(state) { + const { + x, + y + } = state; + const diffCoords = await convertValueToCoords(state, options); + return { + x: x + diffCoords.x, + y: y + diffCoords.y, + data: diffCoords + }; + } + }; +}; +exports.offset = offset; +function getCrossAxis(axis) { + return axis === 'x' ? 'y' : 'x'; +} + +/** + * Optimizes the visibility of the floating element by shifting it in order to + * keep it in view when it will overflow the clipping boundary. + * @see https://floating-ui.com/docs/shift + */ +const shift = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'shift', + options, + async fn(state) { + const { + x, + y, + placement + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = false, + limiter = { + fn: _ref => { + let { + x, + y + } = _ref; + return { + x, + y + }; + } + }, + ...detectOverflowOptions + } = evaluate(options, state); + const coords = { + x, + y + }; + const overflow = await detectOverflow(state, detectOverflowOptions); + const mainAxis = getMainAxisFromPlacement(getSide(placement)); + const crossAxis = getCrossAxis(mainAxis); + let mainAxisCoord = coords[mainAxis]; + let crossAxisCoord = coords[crossAxis]; + if (checkMainAxis) { + const minSide = mainAxis === 'y' ? 'top' : 'left'; + const maxSide = mainAxis === 'y' ? 'bottom' : 'right'; + const min = mainAxisCoord + overflow[minSide]; + const max = mainAxisCoord - overflow[maxSide]; + mainAxisCoord = within(min, mainAxisCoord, max); + } + if (checkCrossAxis) { + const minSide = crossAxis === 'y' ? 'top' : 'left'; + const maxSide = crossAxis === 'y' ? 'bottom' : 'right'; + const min = crossAxisCoord + overflow[minSide]; + const max = crossAxisCoord - overflow[maxSide]; + crossAxisCoord = within(min, crossAxisCoord, max); + } + const limitedCoords = limiter.fn({ + ...state, + [mainAxis]: mainAxisCoord, + [crossAxis]: crossAxisCoord + }); + return { + ...limitedCoords, + data: { + x: limitedCoords.x - x, + y: limitedCoords.y - y + } + }; + } + }; +}; +/** + * Built-in `limiter` that will stop `shift()` at a certain point. + */ +exports.shift = shift; +const limitShift = function (options) { + if (options === void 0) { + options = {}; + } + return { + options, + fn(state) { + const { + x, + y, + placement, + rects, + middlewareData + } = state; + const { + offset = 0, + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = true + } = evaluate(options, state); + const coords = { + x, + y + }; + const mainAxis = getMainAxisFromPlacement(placement); + const crossAxis = getCrossAxis(mainAxis); + let mainAxisCoord = coords[mainAxis]; + let crossAxisCoord = coords[crossAxis]; + const rawOffset = evaluate(offset, state); + const computedOffset = typeof rawOffset === 'number' ? { + mainAxis: rawOffset, + crossAxis: 0 + } : { + mainAxis: 0, + crossAxis: 0, + ...rawOffset + }; + if (checkMainAxis) { + const len = mainAxis === 'y' ? 'height' : 'width'; + const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis; + const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis; + if (mainAxisCoord < limitMin) { + mainAxisCoord = limitMin; + } else if (mainAxisCoord > limitMax) { + mainAxisCoord = limitMax; + } + } + if (checkCrossAxis) { + var _middlewareData$offse, _middlewareData$offse2; + const len = mainAxis === 'y' ? 'width' : 'height'; + const isOriginSide = ['top', 'left'].includes(getSide(placement)); + const limitMin = rects.reference[crossAxis] - rects.floating[len] + (isOriginSide ? ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse[crossAxis]) || 0 : 0) + (isOriginSide ? 0 : computedOffset.crossAxis); + const limitMax = rects.reference[crossAxis] + rects.reference[len] + (isOriginSide ? 0 : ((_middlewareData$offse2 = middlewareData.offset) == null ? void 0 : _middlewareData$offse2[crossAxis]) || 0) - (isOriginSide ? computedOffset.crossAxis : 0); + if (crossAxisCoord < limitMin) { + crossAxisCoord = limitMin; + } else if (crossAxisCoord > limitMax) { + crossAxisCoord = limitMax; + } + } + return { + [mainAxis]: mainAxisCoord, + [crossAxis]: crossAxisCoord + }; + } + }; +}; + +/** + * Provides data that allows you to change the size of the floating element — + * for instance, prevent it from overflowing the clipping boundary or match the + * width of the reference element. + * @see https://floating-ui.com/docs/size + */ +exports.limitShift = limitShift; +const size = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'size', + options, + async fn(state) { + const { + placement, + rects, + platform, + elements + } = state; + const { + apply = () => {}, + ...detectOverflowOptions + } = evaluate(options, state); + const overflow = await detectOverflow(state, detectOverflowOptions); + const side = getSide(placement); + const alignment = getAlignment(placement); + const axis = getMainAxisFromPlacement(placement); + const isXAxis = axis === 'x'; + const { + width, + height + } = rects.floating; + let heightSide; + let widthSide; + if (side === 'top' || side === 'bottom') { + heightSide = side; + widthSide = alignment === ((await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating))) ? 'start' : 'end') ? 'left' : 'right'; + } else { + widthSide = side; + heightSide = alignment === 'end' ? 'top' : 'bottom'; + } + const overflowAvailableHeight = height - overflow[heightSide]; + const overflowAvailableWidth = width - overflow[widthSide]; + const noShift = !state.middlewareData.shift; + let availableHeight = overflowAvailableHeight; + let availableWidth = overflowAvailableWidth; + if (isXAxis) { + const maximumClippingWidth = width - overflow.left - overflow.right; + availableWidth = alignment || noShift ? min(overflowAvailableWidth, maximumClippingWidth) : maximumClippingWidth; + } else { + const maximumClippingHeight = height - overflow.top - overflow.bottom; + availableHeight = alignment || noShift ? min(overflowAvailableHeight, maximumClippingHeight) : maximumClippingHeight; + } + if (noShift && !alignment) { + const xMin = max(overflow.left, 0); + const xMax = max(overflow.right, 0); + const yMin = max(overflow.top, 0); + const yMax = max(overflow.bottom, 0); + if (isXAxis) { + availableWidth = width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right)); + } else { + availableHeight = height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom)); + } + } + await apply({ + ...state, + availableWidth, + availableHeight + }); + const nextDimensions = await platform.getDimensions(elements.floating); + if (width !== nextDimensions.width || height !== nextDimensions.height) { + return { + reset: { + rects: true + } + }; + } + return {}; + } + }; +}; +exports.size = size; + +/***/ }), + +/***/ "../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.esm.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.esm.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "arrow", ({ + enumerable: true, + get: function () { + return _core.arrow; + } +})); +Object.defineProperty(exports, "autoPlacement", ({ + enumerable: true, + get: function () { + return _core.autoPlacement; + } +})); +exports.autoUpdate = autoUpdate; +exports.computePosition = void 0; +Object.defineProperty(exports, "detectOverflow", ({ + enumerable: true, + get: function () { + return _core.detectOverflow; + } +})); +Object.defineProperty(exports, "flip", ({ + enumerable: true, + get: function () { + return _core.flip; + } +})); +exports.getOverflowAncestors = getOverflowAncestors; +Object.defineProperty(exports, "hide", ({ + enumerable: true, + get: function () { + return _core.hide; + } +})); +Object.defineProperty(exports, "inline", ({ + enumerable: true, + get: function () { + return _core.inline; + } +})); +Object.defineProperty(exports, "limitShift", ({ + enumerable: true, + get: function () { + return _core.limitShift; + } +})); +Object.defineProperty(exports, "offset", ({ + enumerable: true, + get: function () { + return _core.offset; + } +})); +exports.platform = void 0; +Object.defineProperty(exports, "shift", ({ + enumerable: true, + get: function () { + return _core.shift; + } +})); +Object.defineProperty(exports, "size", ({ + enumerable: true, + get: function () { + return _core.size; + } +})); +var _core = __webpack_require__(/*! @floating-ui/core */ "../../../node_modules/@floating-ui/core/dist/floating-ui.core.esm.js"); +function getWindow(node) { + var _node$ownerDocument; + return ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; +} +function getComputedStyle$1(element) { + return getWindow(element).getComputedStyle(element); +} +function isNode(value) { + return value instanceof getWindow(value).Node; +} +function getNodeName(node) { + return isNode(node) ? (node.nodeName || '').toLowerCase() : ''; +} +function isHTMLElement(value) { + return value instanceof getWindow(value).HTMLElement; +} +function isElement(value) { + return value instanceof getWindow(value).Element; +} +function isShadowRoot(node) { + // Browsers without `ShadowRoot` support. + if (typeof ShadowRoot === 'undefined') { + return false; + } + const OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; +} +function isOverflowElement(element) { + const { + overflow, + overflowX, + overflowY, + display + } = getComputedStyle$1(element); + return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !['inline', 'contents'].includes(display); +} +function isTableElement(element) { + return ['table', 'td', 'th'].includes(getNodeName(element)); +} +function isContainingBlock(element) { + const safari = isSafari(); + const css = getComputedStyle$1(element); + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + return css.transform !== 'none' || css.perspective !== 'none' || !safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false) || !safari && (css.filter ? css.filter !== 'none' : false) || ['transform', 'perspective', 'filter'].some(value => (css.willChange || '').includes(value)) || ['paint', 'layout', 'strict', 'content'].some(value => (css.contain || '').includes(value)); +} +function isSafari() { + if (typeof CSS === 'undefined' || !CSS.supports) return false; + return CSS.supports('-webkit-backdrop-filter', 'none'); +} +function isLastTraversableNode(node) { + return ['html', 'body', '#document'].includes(getNodeName(node)); +} +const min = Math.min; +const max = Math.max; +const round = Math.round; +function getCssDimensions(element) { + const css = getComputedStyle$1(element); + // In testing environments, the `width` and `height` properties are empty + // strings for SVG elements, returning NaN. Fallback to `0` in this case. + let width = parseFloat(css.width) || 0; + let height = parseFloat(css.height) || 0; + const hasOffset = isHTMLElement(element); + const offsetWidth = hasOffset ? element.offsetWidth : width; + const offsetHeight = hasOffset ? element.offsetHeight : height; + const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; + if (shouldFallback) { + width = offsetWidth; + height = offsetHeight; + } + return { + width, + height, + fallback: shouldFallback + }; +} +function unwrapElement(element) { + return !isElement(element) ? element.contextElement : element; +} +const FALLBACK_SCALE = { + x: 1, + y: 1 +}; +function getScale(element) { + const domElement = unwrapElement(element); + if (!isHTMLElement(domElement)) { + return FALLBACK_SCALE; + } + const rect = domElement.getBoundingClientRect(); + const { + width, + height, + fallback + } = getCssDimensions(domElement); + let x = (fallback ? round(rect.width) : rect.width) / width; + let y = (fallback ? round(rect.height) : rect.height) / height; + + // 0, NaN, or Infinity should always fallback to 1. + + if (!x || !Number.isFinite(x)) { + x = 1; + } + if (!y || !Number.isFinite(y)) { + y = 1; + } + return { + x, + y + }; +} +const noOffsets = { + x: 0, + y: 0 +}; +function getVisualOffsets(element, isFixed, floatingOffsetParent) { + var _win$visualViewport, _win$visualViewport2; + if (isFixed === void 0) { + isFixed = true; + } + if (!isSafari()) { + return noOffsets; + } + const win = element ? getWindow(element) : window; + if (!floatingOffsetParent || isFixed && floatingOffsetParent !== win) { + return noOffsets; + } + return { + x: ((_win$visualViewport = win.visualViewport) == null ? void 0 : _win$visualViewport.offsetLeft) || 0, + y: ((_win$visualViewport2 = win.visualViewport) == null ? void 0 : _win$visualViewport2.offsetTop) || 0 + }; +} +function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { + if (includeScale === void 0) { + includeScale = false; + } + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + const clientRect = element.getBoundingClientRect(); + const domElement = unwrapElement(element); + let scale = FALLBACK_SCALE; + if (includeScale) { + if (offsetParent) { + if (isElement(offsetParent)) { + scale = getScale(offsetParent); + } + } else { + scale = getScale(element); + } + } + const visualOffsets = getVisualOffsets(domElement, isFixedStrategy, offsetParent); + let x = (clientRect.left + visualOffsets.x) / scale.x; + let y = (clientRect.top + visualOffsets.y) / scale.y; + let width = clientRect.width / scale.x; + let height = clientRect.height / scale.y; + if (domElement) { + const win = getWindow(domElement); + const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; + let currentIFrame = win.frameElement; + while (currentIFrame && offsetParent && offsetWin !== win) { + const iframeScale = getScale(currentIFrame); + const iframeRect = currentIFrame.getBoundingClientRect(); + const css = getComputedStyle(currentIFrame); + iframeRect.x += (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; + iframeRect.y += (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; + x *= iframeScale.x; + y *= iframeScale.y; + width *= iframeScale.x; + height *= iframeScale.y; + x += iframeRect.x; + y += iframeRect.y; + currentIFrame = getWindow(currentIFrame).frameElement; + } + } + return (0, _core.rectToClientRect)({ + width, + height, + x, + y + }); +} +function getDocumentElement(node) { + return ((isNode(node) ? node.ownerDocument : node.document) || window.document).documentElement; +} +function getNodeScroll(element) { + if (isElement(element)) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + return { + scrollLeft: element.pageXOffset, + scrollTop: element.pageYOffset + }; +} +function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { + let { + rect, + offsetParent, + strategy + } = _ref; + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + if (offsetParent === documentElement) { + return rect; + } + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + let scale = { + x: 1, + y: 1 + }; + const offsets = { + x: 0, + y: 0 + }; + if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== 'fixed') { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isHTMLElement(offsetParent)) { + const offsetRect = getBoundingClientRect(offsetParent); + scale = getScale(offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } + } + return { + width: rect.width * scale.x, + height: rect.height * scale.y, + x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x, + y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + }; +} +function getWindowScrollBarX(element) { + // If has a CSS width greater than the viewport, then this will be + // incorrect for RTL. + return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft; +} + +// Gets the entire size of the scrollable document area, even extending outside +// of the `` and `` rect bounds if horizontally scrollable. +function getDocumentRect(element) { + const html = getDocumentElement(element); + const scroll = getNodeScroll(element); + const body = element.ownerDocument.body; + const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); + const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); + let x = -scroll.scrollLeft + getWindowScrollBarX(element); + const y = -scroll.scrollTop; + if (getComputedStyle$1(body).direction === 'rtl') { + x += max(html.clientWidth, body.clientWidth) - width; + } + return { + width, + height, + x, + y + }; +} +function getParentNode(node) { + if (getNodeName(node) === 'html') { + return node; + } + const result = + // Step into the shadow DOM of the parent of a slotted node. + node.assignedSlot || + // DOM Element detected. + node.parentNode || + // ShadowRoot detected. + isShadowRoot(node) && node.host || + // Fallback. + getDocumentElement(node); + return isShadowRoot(result) ? result.host : result; +} +function getNearestOverflowAncestor(node) { + const parentNode = getParentNode(node); + if (isLastTraversableNode(parentNode)) { + // `getParentNode` will never return a `Document` due to the fallback + // check, so it's either the or element. + return parentNode.ownerDocument.body; + } + if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { + return parentNode; + } + return getNearestOverflowAncestor(parentNode); +} +function getOverflowAncestors(node, list) { + var _node$ownerDocument; + if (list === void 0) { + list = []; + } + const scrollableAncestor = getNearestOverflowAncestor(node); + const isBody = scrollableAncestor === ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.body); + const win = getWindow(scrollableAncestor); + if (isBody) { + return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : []); + } + return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor)); +} +function getViewportRect(element, strategy) { + const win = getWindow(element); + const html = getDocumentElement(element); + const visualViewport = win.visualViewport; + let width = html.clientWidth; + let height = html.clientHeight; + let x = 0; + let y = 0; + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + const visualViewportBased = isSafari(); + if (!visualViewportBased || visualViewportBased && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + return { + width, + height, + x, + y + }; +} + +// Returns the inner client rect, subtracting scrollbars if present. +function getInnerBoundingClientRect(element, strategy) { + const clientRect = getBoundingClientRect(element, true, strategy === 'fixed'); + const top = clientRect.top + element.clientTop; + const left = clientRect.left + element.clientLeft; + const scale = isHTMLElement(element) ? getScale(element) : { + x: 1, + y: 1 + }; + const width = element.clientWidth * scale.x; + const height = element.clientHeight * scale.y; + const x = left * scale.x; + const y = top * scale.y; + return { + width, + height, + x, + y + }; +} +function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { + let rect; + if (clippingAncestor === 'viewport') { + rect = getViewportRect(element, strategy); + } else if (clippingAncestor === 'document') { + rect = getDocumentRect(getDocumentElement(element)); + } else if (isElement(clippingAncestor)) { + rect = getInnerBoundingClientRect(clippingAncestor, strategy); + } else { + const visualOffsets = getVisualOffsets(element); + rect = { + ...clippingAncestor, + x: clippingAncestor.x - visualOffsets.x, + y: clippingAncestor.y - visualOffsets.y + }; + } + return (0, _core.rectToClientRect)(rect); +} +function hasFixedPositionAncestor(element, stopNode) { + const parentNode = getParentNode(element); + if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { + return false; + } + return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode); +} + +// A "clipping ancestor" is an `overflow` element with the characteristic of +// clipping (or hiding) child elements. This returns all clipping ancestors +// of the given element up the tree. +function getClippingElementAncestors(element, cache) { + const cachedResult = cache.get(element); + if (cachedResult) { + return cachedResult; + } + let result = getOverflowAncestors(element).filter(el => isElement(el) && getNodeName(el) !== 'body'); + let currentContainingBlockComputedStyle = null; + const elementIsFixed = getComputedStyle$1(element).position === 'fixed'; + let currentNode = elementIsFixed ? getParentNode(element) : element; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { + const computedStyle = getComputedStyle$1(currentNode); + const currentNodeIsContaining = isContainingBlock(currentNode); + if (!currentNodeIsContaining && computedStyle.position === 'fixed') { + currentContainingBlockComputedStyle = null; + } + const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && ['absolute', 'fixed'].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); + if (shouldDropCurrentNode) { + // Drop non-containing blocks. + result = result.filter(ancestor => ancestor !== currentNode); + } else { + // Record last containing block for next iteration. + currentContainingBlockComputedStyle = computedStyle; + } + currentNode = getParentNode(currentNode); + } + cache.set(element, result); + return result; +} + +// Gets the maximum area that the element is visible in due to any number of +// clipping ancestors. +function getClippingRect(_ref) { + let { + element, + boundary, + rootBoundary, + strategy + } = _ref; + const elementClippingAncestors = boundary === 'clippingAncestors' ? getClippingElementAncestors(element, this._c) : [].concat(boundary); + const clippingAncestors = [...elementClippingAncestors, rootBoundary]; + const firstClippingAncestor = clippingAncestors[0]; + const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => { + const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy)); + return { + width: clippingRect.right - clippingRect.left, + height: clippingRect.bottom - clippingRect.top, + x: clippingRect.left, + y: clippingRect.top + }; +} +function getDimensions(element) { + return getCssDimensions(element); +} +function getTrueOffsetParent(element, polyfill) { + if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') { + return null; + } + if (polyfill) { + return polyfill(element); + } + return element.offsetParent; +} +function getContainingBlock(element) { + let currentNode = getParentNode(element); + while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { + if (isContainingBlock(currentNode)) { + return currentNode; + } else { + currentNode = getParentNode(currentNode); + } + } + return null; +} + +// Gets the closest ancestor positioned element. Handles some edge cases, +// such as table ancestors and cross browser bugs. +function getOffsetParent(element, polyfill) { + const window = getWindow(element); + if (!isHTMLElement(element)) { + return window; + } + let offsetParent = getTrueOffsetParent(element, polyfill); + while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') { + offsetParent = getTrueOffsetParent(offsetParent, polyfill); + } + if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static' && !isContainingBlock(offsetParent))) { + return window; + } + return offsetParent || getContainingBlock(element) || window; +} +function getRectRelativeToOffsetParent(element, offsetParent, strategy) { + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + const isFixed = strategy === 'fixed'; + const rect = getBoundingClientRect(element, true, isFixed, offsetParent); + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + const offsets = { + x: 0, + y: 0 + }; + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isHTMLElement(offsetParent)) { + const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; +} +const platform = { + getClippingRect, + convertOffsetParentRelativeRectToViewportRelativeRect, + isElement, + getDimensions, + getOffsetParent, + getDocumentElement, + getScale, + async getElementRects(_ref) { + let { + reference, + floating, + strategy + } = _ref; + const getOffsetParentFn = this.getOffsetParent || getOffsetParent; + const getDimensionsFn = this.getDimensions; + return { + reference: getRectRelativeToOffsetParent(reference, await getOffsetParentFn(floating), strategy), + floating: { + x: 0, + y: 0, + ...(await getDimensionsFn(floating)) + } + }; + }, + getClientRects: element => Array.from(element.getClientRects()), + isRTL: element => getComputedStyle$1(element).direction === 'rtl' +}; + +/** + * Automatically updates the position of the floating element when necessary. + * Should only be called when the floating element is mounted on the DOM or + * visible on the screen. + * @returns cleanup function that should be invoked when the floating element is + * removed from the DOM or hidden from the screen. + * @see https://floating-ui.com/docs/autoUpdate + */ +exports.platform = platform; +function autoUpdate(reference, floating, update, options) { + if (options === void 0) { + options = {}; + } + const { + ancestorScroll = true, + ancestorResize = true, + elementResize = true, + animationFrame = false + } = options; + const ancestors = ancestorScroll || ancestorResize ? [...(isElement(reference) ? getOverflowAncestors(reference) : reference.contextElement ? getOverflowAncestors(reference.contextElement) : []), ...getOverflowAncestors(floating)] : []; + ancestors.forEach(ancestor => { + // ignores Window, checks for [object VisualViewport] + const isVisualViewport = !isElement(ancestor) && ancestor.toString().includes('V'); + if (ancestorScroll && (animationFrame ? isVisualViewport : true)) { + ancestor.addEventListener('scroll', update, { + passive: true + }); + } + ancestorResize && ancestor.addEventListener('resize', update); + }); + let observer = null; + if (elementResize) { + observer = new ResizeObserver(() => { + update(); + }); + isElement(reference) && !animationFrame && observer.observe(reference); + if (!isElement(reference) && reference.contextElement && !animationFrame) { + observer.observe(reference.contextElement); + } + observer.observe(floating); + } + let frameId; + let prevRefRect = animationFrame ? getBoundingClientRect(reference) : null; + if (animationFrame) { + frameLoop(); + } + function frameLoop() { + const nextRefRect = getBoundingClientRect(reference); + if (prevRefRect && (nextRefRect.x !== prevRefRect.x || nextRefRect.y !== prevRefRect.y || nextRefRect.width !== prevRefRect.width || nextRefRect.height !== prevRefRect.height)) { + update(); + } + prevRefRect = nextRefRect; + frameId = requestAnimationFrame(frameLoop); + } + update(); + return () => { + var _observer; + ancestors.forEach(ancestor => { + ancestorScroll && ancestor.removeEventListener('scroll', update); + ancestorResize && ancestor.removeEventListener('resize', update); + }); + (_observer = observer) == null ? void 0 : _observer.disconnect(); + observer = null; + if (animationFrame) { + cancelAnimationFrame(frameId); + } + }; +} + +/** + * Computes the `x` and `y` coordinates that will place the floating element + * next to a reference element when it is given a certain CSS positioning + * strategy. + */ +const computePosition = (reference, floating, options) => { + // This caches the expensive `getClippingElementAncestors` function so that + // multiple lifecycle resets re-use the same result. It only lives for a + // single call. If other functions become expensive, we can add them as well. + const cache = new Map(); + const mergedOptions = { + platform, + ...options + }; + const platformWithCache = { + ...mergedOptions.platform, + _c: cache + }; + return (0, _core.computePosition)(reference, floating, { + ...mergedOptions, + platform: platformWithCache + }); +}; +exports.computePosition = computePosition; + +/***/ }), + +/***/ "../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.esm.js": +/*!**************************************************************************************!*\ + !*** ../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.esm.js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.arrow = void 0; +Object.defineProperty(exports, "autoPlacement", ({ + enumerable: true, + get: function () { + return _dom.autoPlacement; + } +})); +Object.defineProperty(exports, "autoUpdate", ({ + enumerable: true, + get: function () { + return _dom.autoUpdate; + } +})); +Object.defineProperty(exports, "computePosition", ({ + enumerable: true, + get: function () { + return _dom.computePosition; + } +})); +Object.defineProperty(exports, "detectOverflow", ({ + enumerable: true, + get: function () { + return _dom.detectOverflow; + } +})); +Object.defineProperty(exports, "flip", ({ + enumerable: true, + get: function () { + return _dom.flip; + } +})); +Object.defineProperty(exports, "getOverflowAncestors", ({ + enumerable: true, + get: function () { + return _dom.getOverflowAncestors; + } +})); +Object.defineProperty(exports, "hide", ({ + enumerable: true, + get: function () { + return _dom.hide; + } +})); +Object.defineProperty(exports, "inline", ({ + enumerable: true, + get: function () { + return _dom.inline; + } +})); +Object.defineProperty(exports, "limitShift", ({ + enumerable: true, + get: function () { + return _dom.limitShift; + } +})); +Object.defineProperty(exports, "offset", ({ + enumerable: true, + get: function () { + return _dom.offset; + } +})); +Object.defineProperty(exports, "platform", ({ + enumerable: true, + get: function () { + return _dom.platform; + } +})); +Object.defineProperty(exports, "shift", ({ + enumerable: true, + get: function () { + return _dom.shift; + } +})); +Object.defineProperty(exports, "size", ({ + enumerable: true, + get: function () { + return _dom.size; + } +})); +exports.useFloating = useFloating; +var _dom = __webpack_require__(/*! @floating-ui/dom */ "../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.esm.js"); +var React = _interopRequireWildcard(__webpack_require__(/*! react */ "react")); +var ReactDOM = _interopRequireWildcard(__webpack_require__(/*! react-dom */ "react-dom")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +/** + * Provides data to position an inner element of the floating element so that it + * appears centered to the reference element. + * This wraps the core `arrow` middleware to allow React refs as the element. + * @see https://floating-ui.com/docs/arrow + */ +const arrow = options => { + function isRef(value) { + return {}.hasOwnProperty.call(value, 'current'); + } + return { + name: 'arrow', + options, + fn(state) { + const { + element, + padding + } = typeof options === 'function' ? options(state) : options; + if (element && isRef(element)) { + if (element.current != null) { + return (0, _dom.arrow)({ + element: element.current, + padding + }).fn(state); + } + return {}; + } else if (element) { + return (0, _dom.arrow)({ + element, + padding + }).fn(state); + } + return {}; + } + }; +}; +exports.arrow = arrow; +var index = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect; + +// Fork of `fast-deep-equal` that only does the comparisons we need and compares +// functions +function deepEqual(a, b) { + if (a === b) { + return true; + } + if (typeof a !== typeof b) { + return false; + } + if (typeof a === 'function' && a.toString() === b.toString()) { + return true; + } + let length, i, keys; + if (a && b && typeof a == 'object') { + if (Array.isArray(a)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0;) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) { + return false; + } + for (i = length; i-- !== 0;) { + if (!{}.hasOwnProperty.call(b, keys[i])) { + return false; + } + } + for (i = length; i-- !== 0;) { + const key = keys[i]; + if (key === '_owner' && a.$$typeof) { + continue; + } + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; + } + return a !== a && b !== b; +} +function getDPR(element) { + if (typeof window === 'undefined') { + return 1; + } + const win = element.ownerDocument.defaultView || window; + return win.devicePixelRatio || 1; +} +function roundByDPR(element, value) { + const dpr = getDPR(element); + return Math.round(value * dpr) / dpr; +} +function useLatestRef(value) { + const ref = React.useRef(value); + index(() => { + ref.current = value; + }); + return ref; +} + +/** + * Provides data to position a floating element. + * @see https://floating-ui.com/docs/react + */ +function useFloating(options) { + if (options === void 0) { + options = {}; + } + const { + placement = 'bottom', + strategy = 'absolute', + middleware = [], + platform, + elements: { + reference: externalReference, + floating: externalFloating + } = {}, + transform = true, + whileElementsMounted, + open + } = options; + const [data, setData] = React.useState({ + x: 0, + y: 0, + strategy, + placement, + middlewareData: {}, + isPositioned: false + }); + const [latestMiddleware, setLatestMiddleware] = React.useState(middleware); + if (!deepEqual(latestMiddleware, middleware)) { + setLatestMiddleware(middleware); + } + const [_reference, _setReference] = React.useState(null); + const [_floating, _setFloating] = React.useState(null); + const setReference = React.useCallback(node => { + if (node != referenceRef.current) { + referenceRef.current = node; + _setReference(node); + } + }, [_setReference]); + const setFloating = React.useCallback(node => { + if (node !== floatingRef.current) { + floatingRef.current = node; + _setFloating(node); + } + }, [_setFloating]); + const referenceEl = externalReference || _reference; + const floatingEl = externalFloating || _floating; + const referenceRef = React.useRef(null); + const floatingRef = React.useRef(null); + const dataRef = React.useRef(data); + const whileElementsMountedRef = useLatestRef(whileElementsMounted); + const platformRef = useLatestRef(platform); + const update = React.useCallback(() => { + if (!referenceRef.current || !floatingRef.current) { + return; + } + const config = { + placement, + strategy, + middleware: latestMiddleware + }; + if (platformRef.current) { + config.platform = platformRef.current; + } + (0, _dom.computePosition)(referenceRef.current, floatingRef.current, config).then(data => { + const fullData = { + ...data, + isPositioned: true + }; + if (isMountedRef.current && !deepEqual(dataRef.current, fullData)) { + dataRef.current = fullData; + ReactDOM.flushSync(() => { + setData(fullData); + }); + } + }); + }, [latestMiddleware, placement, strategy, platformRef]); + index(() => { + if (open === false && dataRef.current.isPositioned) { + dataRef.current.isPositioned = false; + setData(data => ({ + ...data, + isPositioned: false + })); + } + }, [open]); + const isMountedRef = React.useRef(false); + index(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + }; + }, []); + index(() => { + if (referenceEl) referenceRef.current = referenceEl; + if (floatingEl) floatingRef.current = floatingEl; + if (referenceEl && floatingEl) { + if (whileElementsMountedRef.current) { + return whileElementsMountedRef.current(referenceEl, floatingEl, update); + } else { + update(); + } + } + }, [referenceEl, floatingEl, update, whileElementsMountedRef]); + const refs = React.useMemo(() => ({ + reference: referenceRef, + floating: floatingRef, + setReference, + setFloating + }), [setReference, setFloating]); + const elements = React.useMemo(() => ({ + reference: referenceEl, + floating: floatingEl + }), [referenceEl, floatingEl]); + const floatingStyles = React.useMemo(() => { + const initialStyles = { + position: strategy, + left: 0, + top: 0 + }; + if (!elements.floating) { + return initialStyles; + } + const x = roundByDPR(elements.floating, data.x); + const y = roundByDPR(elements.floating, data.y); + if (transform) { + return { + ...initialStyles, + transform: "translate(" + x + "px, " + y + "px)", + ...(getDPR(elements.floating) >= 1.5 && { + willChange: 'transform' + }) + }; + } + return { + position: strategy, + left: x, + top: y + }; + }, [strategy, transform, elements.floating, data.x, data.y]); + return React.useMemo(() => ({ + ...data, + update, + refs, + elements, + floatingStyles + }), [data, update, refs, elements, floatingStyles]); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/animation/dist/Animation.es.js": +/*!***********************************************************************!*\ + !*** ../../../node_modules/@motionone/animation/dist/Animation.es.js ***! + \***********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Animation = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _easingEs = __webpack_require__(/*! ./utils/easing.es.js */ "../../../node_modules/@motionone/animation/dist/utils/easing.es.js"); +class Animation { + constructor(output) { + let keyframes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [0, 1]; + let { + easing, + duration: initialDuration = _utils.defaults.duration, + delay = _utils.defaults.delay, + endDelay = _utils.defaults.endDelay, + repeat = _utils.defaults.repeat, + offset, + direction = "normal" + } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + this.startTime = null; + this.rate = 1; + this.t = 0; + this.cancelTimestamp = null; + this.easing = _utils.noopReturn; + this.duration = 0; + this.totalDuration = 0; + this.repeat = 0; + this.playState = "idle"; + this.finished = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + easing = easing || _utils.defaults.easing; + if ((0, _utils.isEasingGenerator)(easing)) { + const custom = easing.createAnimation(keyframes); + easing = custom.easing; + keyframes = custom.keyframes || keyframes; + initialDuration = custom.duration || initialDuration; + } + this.repeat = repeat; + this.easing = (0, _utils.isEasingList)(easing) ? _utils.noopReturn : (0, _easingEs.getEasingFunction)(easing); + this.updateDuration(initialDuration); + const interpolate$1 = (0, _utils.interpolate)(keyframes, offset, (0, _utils.isEasingList)(easing) ? easing.map(_easingEs.getEasingFunction) : _utils.noopReturn); + this.tick = timestamp => { + var _a; + // TODO: Temporary fix for OptionsResolver typing + delay = delay; + let t = 0; + if (this.pauseTime !== undefined) { + t = this.pauseTime; + } else { + t = (timestamp - this.startTime) * this.rate; + } + this.t = t; + // Convert to seconds + t /= 1000; + // Rebase on delay + t = Math.max(t - delay, 0); + /** + * If this animation has finished, set the current time + * to the total duration. + */ + if (this.playState === "finished" && this.pauseTime === undefined) { + t = this.totalDuration; + } + /** + * Get the current progress (0-1) of the animation. If t is > + * than duration we'll get values like 2.5 (midway through the + * third iteration) + */ + const progress = t / this.duration; + // TODO progress += iterationStart + /** + * Get the current iteration (0 indexed). For instance the floor of + * 2.5 is 2. + */ + let currentIteration = Math.floor(progress); + /** + * Get the current progress of the iteration by taking the remainder + * so 2.5 is 0.5 through iteration 2 + */ + let iterationProgress = progress % 1.0; + if (!iterationProgress && progress >= 1) { + iterationProgress = 1; + } + /** + * If iteration progress is 1 we count that as the end + * of the previous iteration. + */ + iterationProgress === 1 && currentIteration--; + /** + * Reverse progress if we're not running in "normal" direction + */ + const iterationIsOdd = currentIteration % 2; + if (direction === "reverse" || direction === "alternate" && iterationIsOdd || direction === "alternate-reverse" && !iterationIsOdd) { + iterationProgress = 1 - iterationProgress; + } + const p = t >= this.totalDuration ? 1 : Math.min(iterationProgress, 1); + const latest = interpolate$1(this.easing(p)); + output(latest); + const isAnimationFinished = this.pauseTime === undefined && (this.playState === "finished" || t >= this.totalDuration + endDelay); + if (isAnimationFinished) { + this.playState = "finished"; + (_a = this.resolve) === null || _a === void 0 ? void 0 : _a.call(this, latest); + } else if (this.playState !== "idle") { + this.frameRequestId = requestAnimationFrame(this.tick); + } + }; + this.play(); + } + play() { + const now = performance.now(); + this.playState = "running"; + if (this.pauseTime !== undefined) { + this.startTime = now - this.pauseTime; + } else if (!this.startTime) { + this.startTime = now; + } + this.cancelTimestamp = this.startTime; + this.pauseTime = undefined; + this.frameRequestId = requestAnimationFrame(this.tick); + } + pause() { + this.playState = "paused"; + this.pauseTime = this.t; + } + finish() { + this.playState = "finished"; + this.tick(0); + } + stop() { + var _a; + this.playState = "idle"; + if (this.frameRequestId !== undefined) { + cancelAnimationFrame(this.frameRequestId); + } + (_a = this.reject) === null || _a === void 0 ? void 0 : _a.call(this, false); + } + cancel() { + this.stop(); + this.tick(this.cancelTimestamp); + } + reverse() { + this.rate *= -1; + } + commitStyles() {} + updateDuration(duration) { + this.duration = duration; + this.totalDuration = duration * (this.repeat + 1); + } + get currentTime() { + return this.t; + } + set currentTime(t) { + if (this.pauseTime !== undefined || this.rate === 0) { + this.pauseTime = t; + } else { + this.startTime = performance.now() - t / this.rate; + } + } + get playbackRate() { + return this.rate; + } + set playbackRate(rate) { + this.rate = rate; + } +} +exports.Animation = Animation; + +/***/ }), + +/***/ "../../../node_modules/@motionone/animation/dist/index.es.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@motionone/animation/dist/index.es.js ***! + \*******************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "Animation", ({ + enumerable: true, + get: function () { + return _AnimationEs.Animation; + } +})); +Object.defineProperty(exports, "getEasingFunction", ({ + enumerable: true, + get: function () { + return _easingEs.getEasingFunction; + } +})); +var _AnimationEs = __webpack_require__(/*! ./Animation.es.js */ "../../../node_modules/@motionone/animation/dist/Animation.es.js"); +var _easingEs = __webpack_require__(/*! ./utils/easing.es.js */ "../../../node_modules/@motionone/animation/dist/utils/easing.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/animation/dist/utils/easing.es.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@motionone/animation/dist/utils/easing.es.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getEasingFunction = getEasingFunction; +var _easing = __webpack_require__(/*! @motionone/easing */ "../../../node_modules/@motionone/easing/dist/index.es.js"); +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const namedEasings = { + ease: (0, _easing.cubicBezier)(0.25, 0.1, 0.25, 1.0), + "ease-in": (0, _easing.cubicBezier)(0.42, 0.0, 1.0, 1.0), + "ease-in-out": (0, _easing.cubicBezier)(0.42, 0.0, 0.58, 1.0), + "ease-out": (0, _easing.cubicBezier)(0.0, 0.0, 0.58, 1.0) +}; +const functionArgsRegex = /\((.*?)\)/; +function getEasingFunction(definition) { + // If already an easing function, return + if ((0, _utils.isFunction)(definition)) return definition; + // If an easing curve definition, return bezier function + if ((0, _utils.isCubicBezier)(definition)) return (0, _easing.cubicBezier)(...definition); + // If we have a predefined easing function, return + if (namedEasings[definition]) return namedEasings[definition]; + // If this is a steps function, attempt to create easing curve + if (definition.startsWith("steps")) { + const args = functionArgsRegex.exec(definition); + if (args) { + const argsArray = args[1].split(","); + return (0, _easing.steps)(parseFloat(argsArray[0]), argsArray[1].trim()); + } + } + return _utils.noopReturn; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.animateStyle = animateStyle; +var _dataEs = __webpack_require__(/*! ./data.es.js */ "../../../node_modules/@motionone/dom/dist/animate/data.es.js"); +var _cssVarEs = __webpack_require__(/*! ./utils/css-var.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/css-var.es.js"); +var _animation = __webpack_require__(/*! @motionone/animation */ "../../../node_modules/@motionone/animation/dist/index.es.js"); +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _transformsEs = __webpack_require__(/*! ./utils/transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +var _easingEs = __webpack_require__(/*! ./utils/easing.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/easing.es.js"); +var _featureDetectionEs = __webpack_require__(/*! ./utils/feature-detection.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/feature-detection.es.js"); +var _keyframesEs = __webpack_require__(/*! ./utils/keyframes.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/keyframes.es.js"); +var _styleEs = __webpack_require__(/*! ./style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/style.es.js"); +var _getStyleNameEs = __webpack_require__(/*! ./utils/get-style-name.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/get-style-name.es.js"); +var _stopAnimationEs = __webpack_require__(/*! ./utils/stop-animation.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/stop-animation.es.js"); +function getDevToolsRecord() { + return window.__MOTION_DEV_TOOLS_RECORD; +} +function animateStyle(element, key, keyframesDefinition) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + const record = getDevToolsRecord(); + const isRecording = options.record !== false && record; + let animation; + let { + duration = _utils.defaults.duration, + delay = _utils.defaults.delay, + endDelay = _utils.defaults.endDelay, + repeat = _utils.defaults.repeat, + easing = _utils.defaults.easing, + direction, + offset, + allowWebkitAcceleration = false + } = options; + const data = (0, _dataEs.getAnimationData)(element); + let canAnimateNatively = _featureDetectionEs.supports.waapi(); + const valueIsTransform = (0, _transformsEs.isTransform)(key); + /** + * If this is an individual transform, we need to map its + * key to a CSS variable and update the element's transform style + */ + valueIsTransform && (0, _transformsEs.addTransformToElement)(element, key); + const name = (0, _getStyleNameEs.getStyleName)(key); + const motionValue = (0, _dataEs.getMotionValue)(data.values, name); + /** + * Get definition of value, this will be used to convert numerical + * keyframes into the default value type. + */ + const definition = _transformsEs.transformDefinitions.get(name); + /** + * Stop the current animation, if any. Because this will trigger + * commitStyles (DOM writes) and we might later trigger DOM reads, + * this is fired now and we return a factory function to create + * the actual animation that can get called in batch, + */ + (0, _stopAnimationEs.stopAnimation)(motionValue.animation, !((0, _utils.isEasingGenerator)(easing) && motionValue.generator) && options.record !== false); + /** + * Batchable factory function containing all DOM reads. + */ + return () => { + const readInitialValue = () => { + var _a, _b; + return (_b = (_a = _styleEs.style.get(element, name)) !== null && _a !== void 0 ? _a : definition === null || definition === void 0 ? void 0 : definition.initialValue) !== null && _b !== void 0 ? _b : 0; + }; + /** + * Replace null values with the previous keyframe value, or read + * it from the DOM if it's the first keyframe. + */ + let keyframes = (0, _keyframesEs.hydrateKeyframes)((0, _keyframesEs.keyframesList)(keyframesDefinition), readInitialValue); + if ((0, _utils.isEasingGenerator)(easing)) { + const custom = easing.createAnimation(keyframes, readInitialValue, valueIsTransform, name, motionValue); + easing = custom.easing; + if (custom.keyframes !== undefined) keyframes = custom.keyframes; + if (custom.duration !== undefined) duration = custom.duration; + } + /** + * If this is a CSS variable we need to register it with the browser + * before it can be animated natively. We also set it with setProperty + * rather than directly onto the element.style object. + */ + if ((0, _cssVarEs.isCssVar)(name)) { + if (_featureDetectionEs.supports.cssRegisterProperty()) { + (0, _cssVarEs.registerCssVariable)(name); + } else { + canAnimateNatively = false; + } + } + /** + * If we can animate this value with WAAPI, do so. Currently this only + * feature detects CSS.registerProperty but could check WAAPI too. + */ + if (canAnimateNatively) { + /** + * Convert numbers to default value types. Currently this only supports + * transforms but it could also support other value types. + */ + if (definition) { + keyframes = keyframes.map(value => (0, _utils.isNumber)(value) ? definition.toDefaultUnit(value) : value); + } + /** + * If this browser doesn't support partial/implicit keyframes we need to + * explicitly provide one. + */ + if (keyframes.length === 1 && (!_featureDetectionEs.supports.partialKeyframes() || isRecording)) { + keyframes.unshift(readInitialValue()); + } + const animationOptions = { + delay: _utils.time.ms(delay), + duration: _utils.time.ms(duration), + endDelay: _utils.time.ms(endDelay), + easing: !(0, _utils.isEasingList)(easing) ? (0, _easingEs.convertEasing)(easing) : undefined, + direction, + iterations: repeat + 1, + fill: "both" + }; + animation = element.animate({ + [name]: keyframes, + offset, + easing: (0, _utils.isEasingList)(easing) ? easing.map(_easingEs.convertEasing) : undefined + }, animationOptions); + /** + * Polyfill finished Promise in browsers that don't support it + */ + if (!animation.finished) { + animation.finished = new Promise((resolve, reject) => { + animation.onfinish = resolve; + animation.oncancel = reject; + }); + } + const target = keyframes[keyframes.length - 1]; + animation.finished.then(() => { + // Apply styles to target + _styleEs.style.set(element, name, target); + // Ensure fill modes don't persist + animation.cancel(); + }).catch(_utils.noop); + /** + * This forces Webkit to run animations on the main thread by exploiting + * this condition: + * https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/ca/GraphicsLayerCA.cpp?rev=281238#L1099 + * + * This fixes Webkit's timing bugs, like accelerated animations falling + * out of sync with main thread animations and massive delays in starting + * accelerated animations in WKWebView. + */ + if (!allowWebkitAcceleration) animation.playbackRate = 1.000001; + /** + * If we can't animate the value natively then we can fallback to the numbers-only + * polyfill for transforms. + */ + } else if (valueIsTransform) { + /** + * If any keyframe is a string (because we measured it from the DOM), we need to convert + * it into a number before passing to the Animation polyfill. + */ + keyframes = keyframes.map(value => typeof value === "string" ? parseFloat(value) : value); + /** + * If we only have a single keyframe, we need to create an initial keyframe by reading + * the current value from the DOM. + */ + if (keyframes.length === 1) { + keyframes.unshift(parseFloat(readInitialValue())); + } + const render = latest => { + if (definition) latest = definition.toDefaultUnit(latest); + _styleEs.style.set(element, name, latest); + }; + animation = new _animation.Animation(render, keyframes, Object.assign(Object.assign({}, options), { + duration, + easing + })); + } else { + const target = keyframes[keyframes.length - 1]; + _styleEs.style.set(element, name, definition && (0, _utils.isNumber)(target) ? definition.toDefaultUnit(target) : target); + } + if (isRecording) { + record(element, key, keyframes, { + duration, + delay: delay, + easing, + repeat, + offset + }, "motion-one"); + } + motionValue.setAnimation(animation); + return animation; + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/data.es.js": +/*!********************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/data.es.js ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getAnimationData = getAnimationData; +exports.getMotionValue = getMotionValue; +var _types = __webpack_require__(/*! @motionone/types */ "../../../node_modules/@motionone/types/dist/index.es.js"); +const data = new WeakMap(); +function getAnimationData(element) { + if (!data.has(element)) { + data.set(element, { + transforms: [], + values: new Map() + }); + } + return data.get(element); +} +function getMotionValue(motionValues, name) { + if (!motionValues.has(name)) { + motionValues.set(name, new _types.MotionValue()); + } + return motionValues.get(name); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/index.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/index.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.animate = animate; +var _animateStyleEs = __webpack_require__(/*! ./animate-style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js"); +var _optionsEs = __webpack_require__(/*! ./utils/options.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/options.es.js"); +var _resolveElementsEs = __webpack_require__(/*! ../utils/resolve-elements.es.js */ "../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js"); +var _controlsEs = __webpack_require__(/*! ./utils/controls.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/controls.es.js"); +var _staggerEs = __webpack_require__(/*! ../utils/stagger.es.js */ "../../../node_modules/@motionone/dom/dist/utils/stagger.es.js"); +function animate(elements, keyframes) { + let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + elements = (0, _resolveElementsEs.resolveElements)(elements); + const numElements = elements.length; + /** + * Create and start new animations + */ + const animationFactories = []; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + for (const key in keyframes) { + const valueOptions = (0, _optionsEs.getOptions)(options, key); + valueOptions.delay = (0, _staggerEs.resolveOption)(valueOptions.delay, i, numElements); + const animation = (0, _animateStyleEs.animateStyle)(element, key, keyframes[key], valueOptions); + animationFactories.push(animation); + } + } + return (0, _controlsEs.withControls)(animationFactories, options, + /** + * TODO: + * If easing is set to spring or glide, duration will be dynamically + * generated. Ideally we would dynamically generate this from + * animation.effect.getComputedTiming().duration but this isn't + * supported in iOS13 or our number polyfill. Perhaps it's possible + * to Proxy animations returned from animateStyle that has duration + * as a getter. + */ + options.duration); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/style.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/style.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.style = void 0; +var _cssVarEs = __webpack_require__(/*! ./utils/css-var.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/css-var.es.js"); +var _getStyleNameEs = __webpack_require__(/*! ./utils/get-style-name.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/get-style-name.es.js"); +var _transformsEs = __webpack_require__(/*! ./utils/transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +const style = { + get: (element, name) => { + name = (0, _getStyleNameEs.getStyleName)(name); + let value = (0, _cssVarEs.isCssVar)(name) ? element.style.getPropertyValue(name) : getComputedStyle(element)[name]; + if (!value && value !== 0) { + const definition = _transformsEs.transformDefinitions.get(name); + if (definition) value = definition.initialValue; + } + return value; + }, + set: (element, name, value) => { + name = (0, _getStyleNameEs.getStyleName)(name); + if ((0, _cssVarEs.isCssVar)(name)) { + element.style.setProperty(name, value); + } else { + element.style[name] = value; + } + } +}; +exports.style = style; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/controls.es.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/controls.es.js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.withControls = exports.controls = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _stopAnimationEs = __webpack_require__(/*! ./stop-animation.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/stop-animation.es.js"); +const createAnimation = factory => factory(); +const withControls = function (animationFactory, options) { + let duration = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _utils.defaults.duration; + return new Proxy({ + animations: animationFactory.map(createAnimation).filter(Boolean), + duration, + options + }, controls); +}; +/** + * TODO: + * Currently this returns the first animation, ideally it would return + * the first active animation. + */ +exports.withControls = withControls; +const getActiveAnimation = state => state.animations[0]; +const controls = { + get: (target, key) => { + const activeAnimation = getActiveAnimation(target); + switch (key) { + case "duration": + return target.duration; + case "currentTime": + return _utils.time.s((activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key]) || 0); + case "playbackRate": + case "playState": + return activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key]; + case "finished": + if (!target.finished) { + target.finished = Promise.all(target.animations.map(selectFinished)).catch(_utils.noop); + } + return target.finished; + case "stop": + return () => { + target.animations.forEach(animation => (0, _stopAnimationEs.stopAnimation)(animation)); + }; + case "forEachNative": + /** + * This is for internal use only, fire a callback for each + * underlying animation. + */ + return callback => { + target.animations.forEach(animation => callback(animation, target)); + }; + default: + return typeof (activeAnimation === null || activeAnimation === void 0 ? void 0 : activeAnimation[key]) === "undefined" ? undefined : () => target.animations.forEach(animation => animation[key]()); + } + }, + set: (target, key, value) => { + switch (key) { + case "currentTime": + value = _utils.time.ms(value); + case "currentTime": + case "playbackRate": + for (let i = 0; i < target.animations.length; i++) { + target.animations[i][key] = value; + } + return true; + } + return false; + } +}; +exports.controls = controls; +const selectFinished = animation => animation.finished; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/css-var.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/css-var.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isCssVar = void 0; +exports.registerCssVariable = registerCssVariable; +exports.registeredProperties = void 0; +var _transformsEs = __webpack_require__(/*! ./transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +const isCssVar = name => name.startsWith("--"); +exports.isCssVar = isCssVar; +const registeredProperties = new Set(); +exports.registeredProperties = registeredProperties; +function registerCssVariable(name) { + if (registeredProperties.has(name)) return; + registeredProperties.add(name); + try { + const { + syntax, + initialValue + } = _transformsEs.transformDefinitions.has(name) ? _transformsEs.transformDefinitions.get(name) : {}; + CSS.registerProperty({ + name, + inherits: false, + syntax, + initialValue + }); + } catch (e) {} +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/easing.es.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/easing.es.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.cubicBezierAsString = exports.convertEasing = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const convertEasing = easing => (0, _utils.isCubicBezier)(easing) ? cubicBezierAsString(easing) : easing; +exports.convertEasing = convertEasing; +const cubicBezierAsString = _ref => { + let [a, b, c, d] = _ref; + return `cubic-bezier(${a}, ${b}, ${c}, ${d})`; +}; +exports.cubicBezierAsString = cubicBezierAsString; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/feature-detection.es.js": +/*!***************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/feature-detection.es.js ***! + \***************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.supports = void 0; +const testAnimation = keyframes => document.createElement("div").animate(keyframes, { + duration: 0.001 +}); +const featureTests = { + cssRegisterProperty: () => typeof CSS !== "undefined" && Object.hasOwnProperty.call(CSS, "registerProperty"), + waapi: () => Object.hasOwnProperty.call(Element.prototype, "animate"), + partialKeyframes: () => { + try { + testAnimation({ + opacity: [1] + }); + } catch (e) { + return false; + } + return true; + }, + finished: () => Boolean(testAnimation({ + opacity: [0, 1] + }).finished) +}; +const results = {}; +const supports = {}; +exports.supports = supports; +for (const key in featureTests) { + supports[key] = () => { + if (results[key] === undefined) results[key] = featureTests[key](); + return results[key]; + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/get-style-name.es.js": +/*!************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/get-style-name.es.js ***! + \************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getStyleName = getStyleName; +var _transformsEs = __webpack_require__(/*! ./transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +function getStyleName(key) { + if (_transformsEs.transformAlias[key]) key = _transformsEs.transformAlias[key]; + return (0, _transformsEs.isTransform)(key) ? (0, _transformsEs.asTransformCssVar)(key) : key; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/keyframes.es.js": +/*!*******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/keyframes.es.js ***! + \*******************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.hydrateKeyframes = hydrateKeyframes; +exports.keyframesList = void 0; +function hydrateKeyframes(keyframes, readInitialValue) { + for (let i = 0; i < keyframes.length; i++) { + if (keyframes[i] === null) { + keyframes[i] = i ? keyframes[i - 1] : readInitialValue(); + } + } + return keyframes; +} +const keyframesList = keyframes => Array.isArray(keyframes) ? keyframes : [keyframes]; +exports.keyframesList = keyframesList; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/options.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/options.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getOptions = void 0; +const getOptions = (options, key) => +/** + * TODO: Make test for this + * Always return a new object otherwise delay is overwritten by results of stagger + * and this results in no stagger + */ +options[key] ? Object.assign(Object.assign({}, options), options[key]) : Object.assign({}, options); +exports.getOptions = getOptions; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/stop-animation.es.js": +/*!************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/stop-animation.es.js ***! + \************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.stopAnimation = stopAnimation; +function stopAnimation(animation) { + let needsCommit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + if (!animation || animation.playState === "finished") return; + // Suppress error thrown by WAAPI + try { + if (animation.stop) { + animation.stop(); + } else { + needsCommit && animation.commitStyles(); + animation.cancel(); + } + } catch (e) {} +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/style-object.es.js": +/*!**********************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/style-object.es.js ***! + \**********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createStyles = createStyles; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _transformsEs = __webpack_require__(/*! ./transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +function createStyles(keyframes) { + const initialKeyframes = {}; + const transformKeys = []; + for (let key in keyframes) { + const value = keyframes[key]; + if ((0, _transformsEs.isTransform)(key)) { + if (_transformsEs.transformAlias[key]) key = _transformsEs.transformAlias[key]; + transformKeys.push(key); + key = (0, _transformsEs.asTransformCssVar)(key); + } + let initialKeyframe = Array.isArray(value) ? value[0] : value; + /** + * If this is a number and we have a default value type, convert the number + * to this type. + */ + const definition = _transformsEs.transformDefinitions.get(key); + if (definition) { + initialKeyframe = (0, _utils.isNumber)(value) ? definition.toDefaultUnit(value) : value; + } + initialKeyframes[key] = initialKeyframe; + } + if (transformKeys.length) { + initialKeyframes.transform = (0, _transformsEs.buildTransformTemplate)(transformKeys); + } + return initialKeyframes; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/style-string.es.js": +/*!**********************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/style-string.es.js ***! + \**********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createStyleString = createStyleString; +var _styleObjectEs = __webpack_require__(/*! ./style-object.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/style-object.es.js"); +const camelLetterToPipeLetter = letter => `-${letter.toLowerCase()}`; +const camelToPipeCase = str => str.replace(/[A-Z]/g, camelLetterToPipeLetter); +function createStyleString() { + let target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + const styles = (0, _styleObjectEs.createStyles)(target); + let style = ""; + for (const key in styles) { + style += key.startsWith("--") ? key : camelToPipeCase(key); + style += `: ${styles[key]}; `; + } + return style; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js": +/*!********************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js ***! + \********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.transformDefinitions = exports.transformAlias = exports.isTransform = exports.compareTransformOrder = exports.buildTransformTemplate = exports.axes = exports.asTransformCssVar = exports.addTransformToElement = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _dataEs = __webpack_require__(/*! ../data.es.js */ "../../../node_modules/@motionone/dom/dist/animate/data.es.js"); +/** + * A list of all transformable axes. We'll use this list to generated a version + * of each axes for each transform. + */ +const axes = ["", "X", "Y", "Z"]; +/** + * An ordered array of each transformable value. By default, transform values + * will be sorted to this order. + */ +exports.axes = axes; +const order = ["translate", "scale", "rotate", "skew"]; +const transformAlias = { + x: "translateX", + y: "translateY", + z: "translateZ" +}; +exports.transformAlias = transformAlias; +const rotation = { + syntax: "", + initialValue: "0deg", + toDefaultUnit: v => v + "deg" +}; +const baseTransformProperties = { + translate: { + syntax: "", + initialValue: "0px", + toDefaultUnit: v => v + "px" + }, + rotate: rotation, + scale: { + syntax: "", + initialValue: 1, + toDefaultUnit: _utils.noopReturn + }, + skew: rotation +}; +const transformDefinitions = new Map(); +exports.transformDefinitions = transformDefinitions; +const asTransformCssVar = name => `--motion-${name}`; +/** + * Generate a list of every possible transform key + */ +exports.asTransformCssVar = asTransformCssVar; +const transforms = ["x", "y", "z"]; +order.forEach(name => { + axes.forEach(axis => { + transforms.push(name + axis); + transformDefinitions.set(asTransformCssVar(name + axis), baseTransformProperties[name]); + }); +}); +/** + * A function to use with Array.sort to sort transform keys by their default order. + */ +const compareTransformOrder = (a, b) => transforms.indexOf(a) - transforms.indexOf(b); +/** + * Provide a quick way to check if a string is the name of a transform + */ +exports.compareTransformOrder = compareTransformOrder; +const transformLookup = new Set(transforms); +const isTransform = name => transformLookup.has(name); +exports.isTransform = isTransform; +const addTransformToElement = (element, name) => { + // Map x to translateX etc + if (transformAlias[name]) name = transformAlias[name]; + const { + transforms + } = (0, _dataEs.getAnimationData)(element); + (0, _utils.addUniqueItem)(transforms, name); + /** + * TODO: An optimisation here could be to cache the transform in element data + * and only update if this has changed. + */ + element.style.transform = buildTransformTemplate(transforms); +}; +exports.addTransformToElement = addTransformToElement; +const buildTransformTemplate = transforms => transforms.sort(compareTransformOrder).reduce(transformListToString, "").trim(); +exports.buildTransformTemplate = buildTransformTemplate; +const transformListToString = (template, name) => `${template} ${name}(var(${asTransformCssVar(name)}))`; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/easing/create-generator-easing.es.js": +/*!**************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/easing/create-generator-easing.es.js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createGeneratorEasing = createGeneratorEasing; +var _generators = __webpack_require__(/*! @motionone/generators */ "../../../node_modules/@motionone/generators/dist/index.es.js"); +function createGeneratorEasing(createGenerator) { + const keyframesCache = new WeakMap(); + return function () { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + const generatorCache = new Map(); + const getGenerator = function () { + let from = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + let to = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100; + let velocity = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + let isScale = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; + const key = `${from}-${to}-${velocity}-${isScale}`; + if (!generatorCache.has(key)) { + generatorCache.set(key, createGenerator(Object.assign({ + from, + to, + velocity, + restSpeed: isScale ? 0.05 : 2, + restDistance: isScale ? 0.01 : 0.5 + }, options))); + } + return generatorCache.get(key); + }; + const getKeyframes = generator => { + if (!keyframesCache.has(generator)) { + keyframesCache.set(generator, (0, _generators.pregenerateKeyframes)(generator)); + } + return keyframesCache.get(generator); + }; + return { + createAnimation: (keyframes, getOrigin, canUseGenerator, name, motionValue) => { + var _a, _b; + let settings; + const numKeyframes = keyframes.length; + let shouldUseGenerator = canUseGenerator && numKeyframes <= 2 && keyframes.every(isNumberOrNull); + if (shouldUseGenerator) { + const target = keyframes[numKeyframes - 1]; + const unresolvedOrigin = numKeyframes === 1 ? null : keyframes[0]; + let velocity = 0; + let origin = 0; + const prevGenerator = motionValue === null || motionValue === void 0 ? void 0 : motionValue.generator; + if (prevGenerator) { + /** + * If we have a generator for this value we can use it to resolve + * the animations's current value and velocity. + */ + const { + animation, + generatorStartTime + } = motionValue; + const startTime = (animation === null || animation === void 0 ? void 0 : animation.startTime) || generatorStartTime || 0; + const currentTime = (animation === null || animation === void 0 ? void 0 : animation.currentTime) || performance.now() - startTime; + const prevGeneratorCurrent = prevGenerator(currentTime).current; + origin = (_a = unresolvedOrigin) !== null && _a !== void 0 ? _a : prevGeneratorCurrent; + if (numKeyframes === 1 || numKeyframes === 2 && keyframes[0] === null) { + velocity = (0, _generators.calcGeneratorVelocity)(t => prevGenerator(t).current, currentTime, prevGeneratorCurrent); + } + } else { + origin = (_b = unresolvedOrigin) !== null && _b !== void 0 ? _b : parseFloat(getOrigin()); + } + const generator = getGenerator(origin, target, velocity, name === null || name === void 0 ? void 0 : name.includes("scale")); + const keyframesMetadata = getKeyframes(generator); + settings = Object.assign(Object.assign({}, keyframesMetadata), { + easing: "linear" + }); + // TODO Add test for this + if (motionValue) { + motionValue.generator = generator; + motionValue.generatorStartTime = performance.now(); + } + } else { + const keyframesMetadata = getKeyframes(getGenerator(0, 100)); + settings = { + easing: "ease", + duration: keyframesMetadata.overshootDuration + }; + } + return settings; + } + }; + }; +} +const isNumberOrNull = value => typeof value !== "string"; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/easing/glide/index.es.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/easing/glide/index.es.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.glide = void 0; +var _generators = __webpack_require__(/*! @motionone/generators */ "../../../node_modules/@motionone/generators/dist/index.es.js"); +var _createGeneratorEasingEs = __webpack_require__(/*! ../create-generator-easing.es.js */ "../../../node_modules/@motionone/dom/dist/easing/create-generator-easing.es.js"); +const glide = (0, _createGeneratorEasingEs.createGeneratorEasing)(_generators.glide); +exports.glide = glide; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/easing/spring/index.es.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/easing/spring/index.es.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.spring = void 0; +var _generators = __webpack_require__(/*! @motionone/generators */ "../../../node_modules/@motionone/generators/dist/index.es.js"); +var _createGeneratorEasingEs = __webpack_require__(/*! ../create-generator-easing.es.js */ "../../../node_modules/@motionone/dom/dist/easing/create-generator-easing.es.js"); +const spring = (0, _createGeneratorEasingEs.createGeneratorEasing)(_generators.spring); +exports.spring = spring; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/in-view.es.js": +/*!************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/in-view.es.js ***! + \************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.inView = inView; +var _resolveElementsEs = __webpack_require__(/*! ../utils/resolve-elements.es.js */ "../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js"); +const thresholds = { + any: 0, + all: 1 +}; +function inView(elementOrSelector, onStart) { + let { + root, + margin: rootMargin, + amount = "any" + } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + /** + * If this browser doesn't support IntersectionObserver, return a dummy stop function. + * Default triggering of onStart is tricky - it could be used for starting/stopping + * videos, lazy loading content etc. We could provide an option to enable a fallback, or + * provide a fallback callback option. + */ + if (typeof IntersectionObserver === "undefined") { + return () => {}; + } + const elements = (0, _resolveElementsEs.resolveElements)(elementOrSelector); + const activeIntersections = new WeakMap(); + const onIntersectionChange = entries => { + entries.forEach(entry => { + const onEnd = activeIntersections.get(entry.target); + /** + * If there's no change to the intersection, we don't need to + * do anything here. + */ + if (entry.isIntersecting === Boolean(onEnd)) return; + if (entry.isIntersecting) { + const newOnEnd = onStart(entry); + if (typeof newOnEnd === "function") { + activeIntersections.set(entry.target, newOnEnd); + } else { + observer.unobserve(entry.target); + } + } else if (onEnd) { + onEnd(entry); + activeIntersections.delete(entry.target); + } + }); + }; + const observer = new IntersectionObserver(onIntersectionChange, { + root, + rootMargin, + threshold: typeof amount === "number" ? amount : thresholds[amount] + }); + elements.forEach(element => observer.observe(element)); + return () => observer.disconnect(); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/resize/handle-element.es.js": +/*!**************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/resize/handle-element.es.js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resizeElement = resizeElement; +var _resolveElementsEs = __webpack_require__(/*! ../../utils/resolve-elements.es.js */ "../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js"); +const resizeHandlers = new WeakMap(); +let observer; +function getElementSize(target, borderBoxSize) { + if (borderBoxSize) { + const { + inlineSize, + blockSize + } = borderBoxSize[0]; + return { + width: inlineSize, + height: blockSize + }; + } else if (target instanceof SVGElement && "getBBox" in target) { + return target.getBBox(); + } else { + return { + width: target.offsetWidth, + height: target.offsetHeight + }; + } +} +function notifyTarget(_ref) { + let { + target, + contentRect, + borderBoxSize + } = _ref; + var _a; + (_a = resizeHandlers.get(target)) === null || _a === void 0 ? void 0 : _a.forEach(handler => { + handler({ + target, + contentSize: contentRect, + get size() { + return getElementSize(target, borderBoxSize); + } + }); + }); +} +function notifyAll(entries) { + entries.forEach(notifyTarget); +} +function createResizeObserver() { + if (typeof ResizeObserver === "undefined") return; + observer = new ResizeObserver(notifyAll); +} +function resizeElement(target, handler) { + if (!observer) createResizeObserver(); + const elements = (0, _resolveElementsEs.resolveElements)(target); + elements.forEach(element => { + let elementHandlers = resizeHandlers.get(element); + if (!elementHandlers) { + elementHandlers = new Set(); + resizeHandlers.set(element, elementHandlers); + } + elementHandlers.add(handler); + observer === null || observer === void 0 ? void 0 : observer.observe(element); + }); + return () => { + elements.forEach(element => { + const elementHandlers = resizeHandlers.get(element); + elementHandlers === null || elementHandlers === void 0 ? void 0 : elementHandlers.delete(handler); + if (!(elementHandlers === null || elementHandlers === void 0 ? void 0 : elementHandlers.size)) { + observer === null || observer === void 0 ? void 0 : observer.unobserve(element); + } + }); + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/resize/handle-window.es.js": +/*!*************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/resize/handle-window.es.js ***! + \*************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resizeWindow = resizeWindow; +const windowCallbacks = new Set(); +let windowResizeHandler; +function createWindowResizeHandler() { + windowResizeHandler = () => { + const size = { + width: window.innerWidth, + height: window.innerHeight + }; + const info = { + target: window, + size, + contentSize: size + }; + windowCallbacks.forEach(callback => callback(info)); + }; + window.addEventListener("resize", windowResizeHandler); +} +function resizeWindow(callback) { + windowCallbacks.add(callback); + if (!windowResizeHandler) createWindowResizeHandler(); + return () => { + windowCallbacks.delete(callback); + if (!windowCallbacks.size && windowResizeHandler) { + windowResizeHandler = undefined; + } + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/resize/index.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/resize/index.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resize = resize; +var _handleElementEs = __webpack_require__(/*! ./handle-element.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/resize/handle-element.es.js"); +var _handleWindowEs = __webpack_require__(/*! ./handle-window.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/resize/handle-window.es.js"); +function resize(a, b) { + return typeof a === "function" ? (0, _handleWindowEs.resizeWindow)(a) : (0, _handleElementEs.resizeElement)(a, b); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/index.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/index.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.scroll = scroll; +var _tslib = __webpack_require__(/*! tslib */ "../../../node_modules/tslib/tslib.es6.js"); +var _indexEs = __webpack_require__(/*! ../resize/index.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/resize/index.es.js"); +var _infoEs = __webpack_require__(/*! ./info.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/info.es.js"); +var _onScrollHandlerEs = __webpack_require__(/*! ./on-scroll-handler.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/on-scroll-handler.es.js"); +const scrollListeners = new WeakMap(); +const resizeListeners = new WeakMap(); +const onScrollHandlers = new WeakMap(); +const getEventTarget = element => element === document.documentElement ? window : element; +function scroll(onScroll) { + let _a = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var { + container = document.documentElement + } = _a, + options = (0, _tslib.__rest)(_a, ["container"]); + let containerHandlers = onScrollHandlers.get(container); + /** + * Get the onScroll handlers for this container. + * If one isn't found, create a new one. + */ + if (!containerHandlers) { + containerHandlers = new Set(); + onScrollHandlers.set(container, containerHandlers); + } + /** + * Create a new onScroll handler for the provided callback. + */ + const info = (0, _infoEs.createScrollInfo)(); + const containerHandler = (0, _onScrollHandlerEs.createOnScrollHandler)(container, onScroll, info, options); + containerHandlers.add(containerHandler); + /** + * Check if there's a scroll event listener for this container. + * If not, create one. + */ + if (!scrollListeners.has(container)) { + const listener = () => { + const time = performance.now(); + for (const handler of containerHandlers) handler.measure(); + for (const handler of containerHandlers) handler.update(time); + for (const handler of containerHandlers) handler.notify(); + }; + scrollListeners.set(container, listener); + const target = getEventTarget(container); + window.addEventListener("resize", listener, { + passive: true + }); + if (container !== document.documentElement) { + resizeListeners.set(container, (0, _indexEs.resize)(container, listener)); + } + target.addEventListener("scroll", listener, { + passive: true + }); + } + const listener = scrollListeners.get(container); + const onLoadProcesss = requestAnimationFrame(listener); + return () => { + var _a; + if (typeof onScroll !== "function") onScroll.stop(); + cancelAnimationFrame(onLoadProcesss); + /** + * Check if we even have any handlers for this container. + */ + const containerHandlers = onScrollHandlers.get(container); + if (!containerHandlers) return; + containerHandlers.delete(containerHandler); + if (containerHandlers.size) return; + /** + * If no more handlers, remove the scroll listener too. + */ + const listener = scrollListeners.get(container); + scrollListeners.delete(container); + if (listener) { + getEventTarget(container).removeEventListener("scroll", listener); + (_a = resizeListeners.get(container)) === null || _a === void 0 ? void 0 : _a(); + window.removeEventListener("resize", listener); + } + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/info.es.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/info.es.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createScrollInfo = void 0; +exports.updateScrollInfo = updateScrollInfo; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +/** + * A time in milliseconds, beyond which we consider the scroll velocity to be 0. + */ +const maxElapsed = 50; +const createAxisInfo = () => ({ + current: 0, + offset: [], + progress: 0, + scrollLength: 0, + targetOffset: 0, + targetLength: 0, + containerLength: 0, + velocity: 0 +}); +const createScrollInfo = () => ({ + time: 0, + x: createAxisInfo(), + y: createAxisInfo() +}); +exports.createScrollInfo = createScrollInfo; +const keys = { + x: { + length: "Width", + position: "Left" + }, + y: { + length: "Height", + position: "Top" + } +}; +function updateAxisInfo(element, axisName, info, time) { + const axis = info[axisName]; + const { + length, + position + } = keys[axisName]; + const prev = axis.current; + const prevTime = info.time; + axis.current = element["scroll" + position]; + axis.scrollLength = element["scroll" + length] - element["client" + length]; + axis.offset.length = 0; + axis.offset[0] = 0; + axis.offset[1] = axis.scrollLength; + axis.progress = (0, _utils.progress)(0, axis.scrollLength, axis.current); + const elapsed = time - prevTime; + axis.velocity = elapsed > maxElapsed ? 0 : (0, _utils.velocityPerSecond)(axis.current - prev, elapsed); +} +function updateScrollInfo(element, info, time) { + updateAxisInfo(element, "x", info, time); + updateAxisInfo(element, "y", info, time); + info.time = time; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/edge.es.js": +/*!************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/edge.es.js ***! + \************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.namedEdges = void 0; +exports.resolveEdge = resolveEdge; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const namedEdges = { + start: 0, + center: 0.5, + end: 1 +}; +exports.namedEdges = namedEdges; +function resolveEdge(edge, length) { + let inset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + let delta = 0; + /** + * If we have this edge defined as a preset, replace the definition + * with the numerical value. + */ + if (namedEdges[edge] !== undefined) { + edge = namedEdges[edge]; + } + /** + * Handle unit values + */ + if ((0, _utils.isString)(edge)) { + const asNumber = parseFloat(edge); + if (edge.endsWith("px")) { + delta = asNumber; + } else if (edge.endsWith("%")) { + edge = asNumber / 100; + } else if (edge.endsWith("vw")) { + delta = asNumber / 100 * document.documentElement.clientWidth; + } else if (edge.endsWith("vh")) { + delta = asNumber / 100 * document.documentElement.clientHeight; + } else { + edge = asNumber; + } + } + /** + * If the edge is defined as a number, handle as a progress value. + */ + if ((0, _utils.isNumber)(edge)) { + delta = length * edge; + } + return inset + delta; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/index.es.js": +/*!*************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/index.es.js ***! + \*************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resolveOffsets = resolveOffsets; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _insetEs = __webpack_require__(/*! ./inset.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/inset.es.js"); +var _presetsEs = __webpack_require__(/*! ./presets.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/presets.es.js"); +var _offsetEs = __webpack_require__(/*! ./offset.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/offset.es.js"); +const point = { + x: 0, + y: 0 +}; +function resolveOffsets(container, info, options) { + let { + offset: offsetDefinition = _presetsEs.ScrollOffset.All + } = options; + const { + target = container, + axis = "y" + } = options; + const lengthLabel = axis === "y" ? "height" : "width"; + const inset = target !== container ? (0, _insetEs.calcInset)(target, container) : point; + /** + * Measure the target and container. If they're the same thing then we + * use the container's scrollWidth/Height as the target, from there + * all other calculations can remain the same. + */ + const targetSize = target === container ? { + width: container.scrollWidth, + height: container.scrollHeight + } : { + width: target.clientWidth, + height: target.clientHeight + }; + const containerSize = { + width: container.clientWidth, + height: container.clientHeight + }; + /** + * Reset the length of the resolved offset array rather than creating a new one. + * TODO: More reusable data structures for targetSize/containerSize would also be good. + */ + info[axis].offset.length = 0; + /** + * Populate the offset array by resolving the user's offset definition into + * a list of pixel scroll offets. + */ + let hasChanged = !info[axis].interpolate; + const numOffsets = offsetDefinition.length; + for (let i = 0; i < numOffsets; i++) { + const offset = (0, _offsetEs.resolveOffset)(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]); + if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) { + hasChanged = true; + } + info[axis].offset[i] = offset; + } + /** + * If the pixel scroll offsets have changed, create a new interpolator function + * to map scroll value into a progress. + */ + if (hasChanged) { + info[axis].interpolate = (0, _utils.interpolate)((0, _utils.defaultOffset)(numOffsets), info[axis].offset); + info[axis].interpolatorOffsets = [...info[axis].offset]; + } + info[axis].progress = info[axis].interpolate(info[axis].current); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/inset.es.js": +/*!*************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/inset.es.js ***! + \*************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.calcInset = calcInset; +function calcInset(element, container) { + let inset = { + x: 0, + y: 0 + }; + let current = element; + while (current && current !== container) { + if (current instanceof HTMLElement) { + inset.x += current.offsetLeft; + inset.y += current.offsetTop; + current = current.offsetParent; + } else if (current instanceof SVGGraphicsElement && "getBBox" in current) { + const { + top, + left + } = current.getBBox(); + inset.x += left; + inset.y += top; + /** + * Assign the next parent element as the tag. + */ + while (current && current.tagName !== "svg") { + current = current.parentNode; + } + } + } + return inset; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/offset.es.js": +/*!**************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/offset.es.js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resolveOffset = resolveOffset; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _edgeEs = __webpack_require__(/*! ./edge.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/edge.es.js"); +const defaultOffset = [0, 0]; +function resolveOffset(offset, containerLength, targetLength, targetInset) { + let offsetDefinition = Array.isArray(offset) ? offset : defaultOffset; + let targetPoint = 0; + let containerPoint = 0; + if ((0, _utils.isNumber)(offset)) { + /** + * If we're provided offset: [0, 0.5, 1] then each number x should become + * [x, x], so we default to the behaviour of mapping 0 => 0 of both target + * and container etc. + */ + offsetDefinition = [offset, offset]; + } else if ((0, _utils.isString)(offset)) { + offset = offset.trim(); + if (offset.includes(" ")) { + offsetDefinition = offset.split(" "); + } else { + /** + * If we're provided a definition like "100px" then we want to apply + * that only to the top of the target point, leaving the container at 0. + * Whereas a named offset like "end" should be applied to both. + */ + offsetDefinition = [offset, _edgeEs.namedEdges[offset] ? offset : `0`]; + } + } + targetPoint = (0, _edgeEs.resolveEdge)(offsetDefinition[0], targetLength, targetInset); + containerPoint = (0, _edgeEs.resolveEdge)(offsetDefinition[1], containerLength); + return targetPoint - containerPoint; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/presets.es.js": +/*!***************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/presets.es.js ***! + \***************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.ScrollOffset = void 0; +const ScrollOffset = { + Enter: [[0, 1], [1, 1]], + Exit: [[0, 0], [1, 0]], + Any: [[1, 0], [0, 1]], + All: [[0, 0], [1, 1]] +}; +exports.ScrollOffset = ScrollOffset; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/gestures/scroll/on-scroll-handler.es.js": +/*!*****************************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/gestures/scroll/on-scroll-handler.es.js ***! + \*****************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createOnScrollHandler = createOnScrollHandler; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _infoEs = __webpack_require__(/*! ./info.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/info.es.js"); +var _indexEs = __webpack_require__(/*! ./offsets/index.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/index.es.js"); +function measure(container) { + let target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : container; + let info = arguments.length > 2 ? arguments[2] : undefined; + /** + * Find inset of target within scrollable container + */ + info.x.targetOffset = 0; + info.y.targetOffset = 0; + if (target !== container) { + let node = target; + while (node && node != container) { + info.x.targetOffset += node.offsetLeft; + info.y.targetOffset += node.offsetTop; + node = node.offsetParent; + } + } + info.x.targetLength = target === container ? target.scrollWidth : target.clientWidth; + info.y.targetLength = target === container ? target.scrollHeight : target.clientHeight; + info.x.containerLength = container.clientWidth; + info.y.containerLength = container.clientHeight; +} +function createOnScrollHandler(element, onScroll, info) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + const axis = options.axis || "y"; + return { + measure: () => measure(element, options.target, info), + update: time => { + (0, _infoEs.updateScrollInfo)(element, info, time); + if (options.offset || options.target) { + (0, _indexEs.resolveOffsets)(element, info, options); + } + }, + notify: typeof onScroll === "function" ? () => onScroll(info) : scrubAnimation(onScroll, info[axis]) + }; +} +function scrubAnimation(controls, axisInfo) { + controls.pause(); + controls.forEachNative((animation, _ref) => { + let { + easing + } = _ref; + var _a, _b; + if (animation.updateDuration) { + if (!easing) animation.easing = _utils.noopReturn; + animation.updateDuration(1); + } else { + const timingOptions = { + duration: 1000 + }; + if (!easing) timingOptions.easing = "linear"; + (_b = (_a = animation.effect) === null || _a === void 0 ? void 0 : _a.updateTiming) === null || _b === void 0 ? void 0 : _b.call(_a, timingOptions); + } + }); + return () => { + controls.currentTime = axisInfo.progress; + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/index.es.js": +/*!*************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/index.es.js ***! + \*************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "ScrollOffset", ({ + enumerable: true, + get: function () { + return _presetsEs.ScrollOffset; + } +})); +Object.defineProperty(exports, "animate", ({ + enumerable: true, + get: function () { + return _indexEs.animate; + } +})); +Object.defineProperty(exports, "animateStyle", ({ + enumerable: true, + get: function () { + return _animateStyleEs.animateStyle; + } +})); +Object.defineProperty(exports, "createMotionState", ({ + enumerable: true, + get: function () { + return _indexEs7.createMotionState; + } +})); +Object.defineProperty(exports, "createStyleString", ({ + enumerable: true, + get: function () { + return _styleStringEs.createStyleString; + } +})); +Object.defineProperty(exports, "createStyles", ({ + enumerable: true, + get: function () { + return _styleObjectEs.createStyles; + } +})); +Object.defineProperty(exports, "getAnimationData", ({ + enumerable: true, + get: function () { + return _dataEs.getAnimationData; + } +})); +Object.defineProperty(exports, "getStyleName", ({ + enumerable: true, + get: function () { + return _getStyleNameEs.getStyleName; + } +})); +Object.defineProperty(exports, "glide", ({ + enumerable: true, + get: function () { + return _indexEs4.glide; + } +})); +Object.defineProperty(exports, "inView", ({ + enumerable: true, + get: function () { + return _inViewEs.inView; + } +})); +Object.defineProperty(exports, "mountedStates", ({ + enumerable: true, + get: function () { + return _indexEs7.mountedStates; + } +})); +Object.defineProperty(exports, "resize", ({ + enumerable: true, + get: function () { + return _indexEs5.resize; + } +})); +Object.defineProperty(exports, "scroll", ({ + enumerable: true, + get: function () { + return _indexEs6.scroll; + } +})); +Object.defineProperty(exports, "spring", ({ + enumerable: true, + get: function () { + return _indexEs3.spring; + } +})); +Object.defineProperty(exports, "stagger", ({ + enumerable: true, + get: function () { + return _staggerEs.stagger; + } +})); +Object.defineProperty(exports, "style", ({ + enumerable: true, + get: function () { + return _styleEs.style; + } +})); +Object.defineProperty(exports, "timeline", ({ + enumerable: true, + get: function () { + return _indexEs2.timeline; + } +})); +Object.defineProperty(exports, "withControls", ({ + enumerable: true, + get: function () { + return _controlsEs.withControls; + } +})); +var _indexEs = __webpack_require__(/*! ./animate/index.es.js */ "../../../node_modules/@motionone/dom/dist/animate/index.es.js"); +var _animateStyleEs = __webpack_require__(/*! ./animate/animate-style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js"); +var _indexEs2 = __webpack_require__(/*! ./timeline/index.es.js */ "../../../node_modules/@motionone/dom/dist/timeline/index.es.js"); +var _staggerEs = __webpack_require__(/*! ./utils/stagger.es.js */ "../../../node_modules/@motionone/dom/dist/utils/stagger.es.js"); +var _indexEs3 = __webpack_require__(/*! ./easing/spring/index.es.js */ "../../../node_modules/@motionone/dom/dist/easing/spring/index.es.js"); +var _indexEs4 = __webpack_require__(/*! ./easing/glide/index.es.js */ "../../../node_modules/@motionone/dom/dist/easing/glide/index.es.js"); +var _styleEs = __webpack_require__(/*! ./animate/style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/style.es.js"); +var _inViewEs = __webpack_require__(/*! ./gestures/in-view.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/in-view.es.js"); +var _indexEs5 = __webpack_require__(/*! ./gestures/resize/index.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/resize/index.es.js"); +var _indexEs6 = __webpack_require__(/*! ./gestures/scroll/index.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/index.es.js"); +var _presetsEs = __webpack_require__(/*! ./gestures/scroll/offsets/presets.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/scroll/offsets/presets.es.js"); +var _controlsEs = __webpack_require__(/*! ./animate/utils/controls.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/controls.es.js"); +var _dataEs = __webpack_require__(/*! ./animate/data.es.js */ "../../../node_modules/@motionone/dom/dist/animate/data.es.js"); +var _getStyleNameEs = __webpack_require__(/*! ./animate/utils/get-style-name.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/get-style-name.es.js"); +var _indexEs7 = __webpack_require__(/*! ./state/index.es.js */ "../../../node_modules/@motionone/dom/dist/state/index.es.js"); +var _styleObjectEs = __webpack_require__(/*! ./animate/utils/style-object.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/style-object.es.js"); +var _styleStringEs = __webpack_require__(/*! ./animate/utils/style-string.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/style-string.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/gestures/hover.es.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/gestures/hover.es.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.hover = void 0; +var _eventsEs = __webpack_require__(/*! ../utils/events.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/events.es.js"); +const mouseEvent = (element, name, action) => event => { + if (event.pointerType && event.pointerType !== "mouse") return; + action(); + (0, _eventsEs.dispatchPointerEvent)(element, name, event); +}; +const hover = { + isActive: options => Boolean(options.hover), + subscribe: (element, _ref) => { + let { + enable, + disable + } = _ref; + const onEnter = mouseEvent(element, "hoverstart", enable); + const onLeave = mouseEvent(element, "hoverend", disable); + element.addEventListener("pointerenter", onEnter); + element.addEventListener("pointerleave", onLeave); + return () => { + element.removeEventListener("pointerenter", onEnter); + element.removeEventListener("pointerleave", onLeave); + }; + } +}; +exports.hover = hover; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/gestures/in-view.es.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/gestures/in-view.es.js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.inView = void 0; +var _tslib = __webpack_require__(/*! tslib */ "../../../node_modules/tslib/tslib.es6.js"); +var _eventsEs = __webpack_require__(/*! ../utils/events.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/events.es.js"); +var _inViewEs = __webpack_require__(/*! ../../gestures/in-view.es.js */ "../../../node_modules/@motionone/dom/dist/gestures/in-view.es.js"); +const inView = { + isActive: options => Boolean(options.inView), + subscribe: (element, _ref, _ref2) => { + let { + enable, + disable + } = _ref; + let { + inViewOptions = {} + } = _ref2; + const { + once + } = inViewOptions, + viewOptions = (0, _tslib.__rest)(inViewOptions, ["once"]); + return (0, _inViewEs.inView)(element, enterEntry => { + enable(); + (0, _eventsEs.dispatchViewEvent)(element, "viewenter", enterEntry); + if (!once) { + return leaveEntry => { + disable(); + (0, _eventsEs.dispatchViewEvent)(element, "viewleave", leaveEntry); + }; + } + }, viewOptions); + } +}; +exports.inView = inView; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/gestures/press.es.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/gestures/press.es.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.press = void 0; +var _eventsEs = __webpack_require__(/*! ../utils/events.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/events.es.js"); +const press = { + isActive: options => Boolean(options.press), + subscribe: (element, _ref) => { + let { + enable, + disable + } = _ref; + const onPointerUp = event => { + disable(); + (0, _eventsEs.dispatchPointerEvent)(element, "pressend", event); + window.removeEventListener("pointerup", onPointerUp); + }; + const onPointerDown = event => { + enable(); + (0, _eventsEs.dispatchPointerEvent)(element, "pressstart", event); + window.addEventListener("pointerup", onPointerUp); + }; + element.addEventListener("pointerdown", onPointerDown); + return () => { + element.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("pointerup", onPointerUp); + }; + } +}; +exports.press = press; + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/index.es.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/index.es.js ***! + \*******************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createMotionState = createMotionState; +exports.mountedStates = void 0; +var _tslib = __webpack_require__(/*! tslib */ "../../../node_modules/tslib/tslib.es6.js"); +var _heyListen = __webpack_require__(/*! hey-listen */ "../../../node_modules/hey-listen/dist/hey-listen.es.js"); +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _animateStyleEs = __webpack_require__(/*! ../animate/animate-style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js"); +var _styleEs = __webpack_require__(/*! ../animate/style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/style.es.js"); +var _optionsEs = __webpack_require__(/*! ../animate/utils/options.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/options.es.js"); +var _hasChangedEs = __webpack_require__(/*! ./utils/has-changed.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/has-changed.es.js"); +var _resolveVariantEs = __webpack_require__(/*! ./utils/resolve-variant.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/resolve-variant.es.js"); +var _scheduleEs = __webpack_require__(/*! ./utils/schedule.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/schedule.es.js"); +var _inViewEs = __webpack_require__(/*! ./gestures/in-view.es.js */ "../../../node_modules/@motionone/dom/dist/state/gestures/in-view.es.js"); +var _hoverEs = __webpack_require__(/*! ./gestures/hover.es.js */ "../../../node_modules/@motionone/dom/dist/state/gestures/hover.es.js"); +var _pressEs = __webpack_require__(/*! ./gestures/press.es.js */ "../../../node_modules/@motionone/dom/dist/state/gestures/press.es.js"); +var _eventsEs = __webpack_require__(/*! ./utils/events.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/events.es.js"); +const gestures = { + inView: _inViewEs.inView, + hover: _hoverEs.hover, + press: _pressEs.press +}; +/** + * A list of state types, in priority order. If a value is defined in + * a righter-most type, it will override any definition in a lefter-most. + */ +const stateTypes = ["initial", "animate", ...Object.keys(gestures), "exit"]; +/** + * A global store of all generated motion states. This can be used to lookup + * a motion state for a given Element. + */ +const mountedStates = new WeakMap(); +exports.mountedStates = mountedStates; +function createMotionState() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + let parent = arguments.length > 1 ? arguments[1] : undefined; + /** + * The element represented by the motion state. This is an empty reference + * when we create the state to support SSR and allow for later mounting + * in view libraries. + * + * @ts-ignore + */ + let element; + /** + * Calculate a depth that we can use to order motion states by tree depth. + */ + let depth = parent ? parent.getDepth() + 1 : 0; + /** + * Track which states are currently active. + */ + const activeStates = { + initial: true, + animate: true + }; + /** + * A map of functions that, when called, will remove event listeners for + * a given gesture. + */ + const gestureSubscriptions = {}; + /** + * Initialise a context to share through motion states. This + * will be populated by variant names (if any). + */ + const context = {}; + for (const name of stateTypes) { + context[name] = typeof options[name] === "string" ? options[name] : parent === null || parent === void 0 ? void 0 : parent.getContext()[name]; + } + /** + * If initial is set to false we use the animate prop as the initial + * animation state. + */ + const initialVariantSource = options.initial === false ? "animate" : "initial"; + /** + * Destructure an initial target out from the resolved initial variant. + */ + let _a = (0, _resolveVariantEs.resolveVariant)(options[initialVariantSource] || context[initialVariantSource], options.variants) || {}, + target = (0, _tslib.__rest)(_a, ["transition"]); + /** + * The base target is a cached map of values that we'll use to animate + * back to if a value is removed from all active state types. This + * is usually the initial value as read from the DOM, for instance if + * it hasn't been defined in initial. + */ + const baseTarget = Object.assign({}, target); + /** + * A generator that will be processed by the global animation scheduler. + * This yeilds when it switches from reading the DOM to writing to it + * to prevent layout thrashing. + */ + function* animateUpdates() { + var _a, _b; + const prevTarget = target; + target = {}; + const animationOptions = {}; + for (const name of stateTypes) { + if (!activeStates[name]) continue; + const variant = (0, _resolveVariantEs.resolveVariant)(options[name]); + if (!variant) continue; + for (const key in variant) { + if (key === "transition") continue; + target[key] = variant[key]; + animationOptions[key] = (0, _optionsEs.getOptions)((_b = (_a = variant.transition) !== null && _a !== void 0 ? _a : options.transition) !== null && _b !== void 0 ? _b : {}, key); + } + } + const allTargetKeys = new Set([...Object.keys(target), ...Object.keys(prevTarget)]); + const animationFactories = []; + allTargetKeys.forEach(key => { + var _a; + if (target[key] === undefined) { + target[key] = baseTarget[key]; + } + if ((0, _hasChangedEs.hasChanged)(prevTarget[key], target[key])) { + (_a = baseTarget[key]) !== null && _a !== void 0 ? _a : baseTarget[key] = _styleEs.style.get(element, key); + animationFactories.push((0, _animateStyleEs.animateStyle)(element, key, target[key], animationOptions[key])); + } + }); + // Wait for all animation states to read from the DOM + yield; + const animations = animationFactories.map(factory => factory()).filter(Boolean); + if (!animations.length) return; + const animationTarget = target; + element.dispatchEvent((0, _eventsEs.motionEvent)("motionstart", animationTarget)); + Promise.all(animations.map(animation => animation.finished)).then(() => { + element.dispatchEvent((0, _eventsEs.motionEvent)("motioncomplete", animationTarget)); + }).catch(_utils.noop); + } + const setGesture = (name, isActive) => () => { + activeStates[name] = isActive; + (0, _scheduleEs.scheduleAnimation)(state); + }; + const updateGestureSubscriptions = () => { + for (const name in gestures) { + const isGestureActive = gestures[name].isActive(options); + const remove = gestureSubscriptions[name]; + if (isGestureActive && !remove) { + gestureSubscriptions[name] = gestures[name].subscribe(element, { + enable: setGesture(name, true), + disable: setGesture(name, false) + }, options); + } else if (!isGestureActive && remove) { + remove(); + delete gestureSubscriptions[name]; + } + } + }; + const state = { + update: newOptions => { + if (!element) return; + options = newOptions; + updateGestureSubscriptions(); + (0, _scheduleEs.scheduleAnimation)(state); + }, + setActive: (name, isActive) => { + if (!element) return; + activeStates[name] = isActive; + (0, _scheduleEs.scheduleAnimation)(state); + }, + animateUpdates, + getDepth: () => depth, + getTarget: () => target, + getOptions: () => options, + getContext: () => context, + mount: newElement => { + (0, _heyListen.invariant)(Boolean(newElement), "Animation state must be mounted with valid Element"); + element = newElement; + mountedStates.set(element, state); + updateGestureSubscriptions(); + return () => { + mountedStates.delete(element); + (0, _scheduleEs.unscheduleAnimation)(state); + for (const key in gestureSubscriptions) { + gestureSubscriptions[key](); + } + }; + }, + isMounted: () => Boolean(element) + }; + return state; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/utils/events.es.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/utils/events.es.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.dispatchPointerEvent = dispatchPointerEvent; +exports.dispatchViewEvent = dispatchViewEvent; +exports.motionEvent = void 0; +const motionEvent = (name, target) => new CustomEvent(name, { + detail: { + target + } +}); +exports.motionEvent = motionEvent; +function dispatchPointerEvent(element, name, event) { + element.dispatchEvent(new CustomEvent(name, { + detail: { + originalEvent: event + } + })); +} +function dispatchViewEvent(element, name, entry) { + element.dispatchEvent(new CustomEvent(name, { + detail: { + originalEntry: entry + } + })); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/utils/has-changed.es.js": +/*!*******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/utils/has-changed.es.js ***! + \*******************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.hasChanged = hasChanged; +exports.shallowCompare = shallowCompare; +function hasChanged(a, b) { + if (typeof a !== typeof b) return true; + if (Array.isArray(a) && Array.isArray(b)) return !shallowCompare(a, b); + return a !== b; +} +function shallowCompare(next, prev) { + const prevLength = prev.length; + if (prevLength !== next.length) return false; + for (let i = 0; i < prevLength; i++) { + if (prev[i] !== next[i]) return false; + } + return true; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/utils/is-variant.es.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/utils/is-variant.es.js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isVariant = isVariant; +function isVariant(definition) { + return typeof definition === "object"; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/utils/resolve-variant.es.js": +/*!***********************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/utils/resolve-variant.es.js ***! + \***********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resolveVariant = resolveVariant; +var _isVariantEs = __webpack_require__(/*! ./is-variant.es.js */ "../../../node_modules/@motionone/dom/dist/state/utils/is-variant.es.js"); +function resolveVariant(definition, variants) { + if ((0, _isVariantEs.isVariant)(definition)) { + return definition; + } else if (definition && variants) { + return variants[definition]; + } +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/state/utils/schedule.es.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/state/utils/schedule.es.js ***! + \****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.scheduleAnimation = scheduleAnimation; +exports.unscheduleAnimation = unscheduleAnimation; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +let scheduled = undefined; +function processScheduledAnimations() { + if (!scheduled) return; + const generators = scheduled.sort(compareByDepth).map(fireAnimateUpdates); + generators.forEach(fireNext); + generators.forEach(fireNext); + scheduled = undefined; +} +function scheduleAnimation(state) { + if (!scheduled) { + scheduled = [state]; + requestAnimationFrame(processScheduledAnimations); + } else { + (0, _utils.addUniqueItem)(scheduled, state); + } +} +function unscheduleAnimation(state) { + scheduled && (0, _utils.removeItem)(scheduled, state); +} +const compareByDepth = (a, b) => a.getDepth() - b.getDepth(); +const fireAnimateUpdates = state => state.animateUpdates(); +const fireNext = iterator => iterator.next(); + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/timeline/index.es.js": +/*!**********************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/timeline/index.es.js ***! + \**********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createAnimationsFromTimeline = createAnimationsFromTimeline; +exports.timeline = timeline; +var _tslib = __webpack_require__(/*! tslib */ "../../../node_modules/tslib/tslib.es6.js"); +var _heyListen = __webpack_require__(/*! hey-listen */ "../../../node_modules/hey-listen/dist/hey-listen.es.js"); +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _staggerEs = __webpack_require__(/*! ../utils/stagger.es.js */ "../../../node_modules/@motionone/dom/dist/utils/stagger.es.js"); +var _animateStyleEs = __webpack_require__(/*! ../animate/animate-style.es.js */ "../../../node_modules/@motionone/dom/dist/animate/animate-style.es.js"); +var _controlsEs = __webpack_require__(/*! ../animate/utils/controls.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/controls.es.js"); +var _keyframesEs = __webpack_require__(/*! ../animate/utils/keyframes.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/keyframes.es.js"); +var _optionsEs = __webpack_require__(/*! ../animate/utils/options.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/options.es.js"); +var _resolveElementsEs = __webpack_require__(/*! ../utils/resolve-elements.es.js */ "../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js"); +var _transformsEs = __webpack_require__(/*! ../animate/utils/transforms.es.js */ "../../../node_modules/@motionone/dom/dist/animate/utils/transforms.es.js"); +var _calcTimeEs = __webpack_require__(/*! ./utils/calc-time.es.js */ "../../../node_modules/@motionone/dom/dist/timeline/utils/calc-time.es.js"); +var _editEs = __webpack_require__(/*! ./utils/edit.es.js */ "../../../node_modules/@motionone/dom/dist/timeline/utils/edit.es.js"); +var _sortEs = __webpack_require__(/*! ./utils/sort.es.js */ "../../../node_modules/@motionone/dom/dist/timeline/utils/sort.es.js"); +function timeline(definition) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _a; + const animationDefinitions = createAnimationsFromTimeline(definition, options); + /** + * Create and start animations + */ + const animationFactories = animationDefinitions.map(definition => (0, _animateStyleEs.animateStyle)(...definition)).filter(Boolean); + return (0, _controlsEs.withControls)(animationFactories, options, + // Get the duration from the first animation definition + (_a = animationDefinitions[0]) === null || _a === void 0 ? void 0 : _a[3].duration); +} +function createAnimationsFromTimeline(definition) { + let _a = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var { + defaultOptions = {} + } = _a, + timelineOptions = (0, _tslib.__rest)(_a, ["defaultOptions"]); + const animationDefinitions = []; + const elementSequences = new Map(); + const elementCache = {}; + const timeLabels = new Map(); + let prevTime = 0; + let currentTime = 0; + let totalDuration = 0; + /** + * Build the timeline by mapping over the definition array and converting + * the definitions into keyframes and offsets with absolute time values. + * These will later get converted into relative offsets in a second pass. + */ + for (let i = 0; i < definition.length; i++) { + const segment = definition[i]; + /** + * If this is a timeline label, mark it and skip the rest of this iteration. + */ + if ((0, _utils.isString)(segment)) { + timeLabels.set(segment, currentTime); + continue; + } else if (!Array.isArray(segment)) { + timeLabels.set(segment.name, (0, _calcTimeEs.calcNextTime)(currentTime, segment.at, prevTime, timeLabels)); + continue; + } + const [elementDefinition, keyframes, options = {}] = segment; + /** + * If a relative or absolute time value has been specified we need to resolve + * it in relation to the currentTime. + */ + if (options.at !== undefined) { + currentTime = (0, _calcTimeEs.calcNextTime)(currentTime, options.at, prevTime, timeLabels); + } + /** + * Keep track of the maximum duration in this definition. This will be + * applied to currentTime once the definition has been parsed. + */ + let maxDuration = 0; + /** + * Find all the elements specified in the definition and parse value + * keyframes from their timeline definitions. + */ + const elements = (0, _resolveElementsEs.resolveElements)(elementDefinition, elementCache); + const numElements = elements.length; + for (let elementIndex = 0; elementIndex < numElements; elementIndex++) { + const element = elements[elementIndex]; + const elementSequence = getElementSequence(element, elementSequences); + for (const key in keyframes) { + const valueSequence = getValueSequence(key, elementSequence); + let valueKeyframes = (0, _keyframesEs.keyframesList)(keyframes[key]); + const valueOptions = (0, _optionsEs.getOptions)(options, key); + let { + duration = defaultOptions.duration || _utils.defaults.duration, + easing = defaultOptions.easing || _utils.defaults.easing + } = valueOptions; + if ((0, _utils.isEasingGenerator)(easing)) { + const valueIsTransform = (0, _transformsEs.isTransform)(key); + (0, _heyListen.invariant)(valueKeyframes.length === 2 || !valueIsTransform, "spring must be provided 2 keyframes within timeline"); + const custom = easing.createAnimation(valueKeyframes, + // TODO We currently only support explicit keyframes + // so this doesn't currently read from the DOM + () => "0", valueIsTransform); + easing = custom.easing; + if (custom.keyframes !== undefined) valueKeyframes = custom.keyframes; + if (custom.duration !== undefined) duration = custom.duration; + } + const delay = (0, _staggerEs.resolveOption)(options.delay, elementIndex, numElements) || 0; + const startTime = currentTime + delay; + const targetTime = startTime + duration; + /** + * + */ + let { + offset = (0, _utils.defaultOffset)(valueKeyframes.length) + } = valueOptions; + /** + * If there's only one offset of 0, fill in a second with length 1 + * + * TODO: Ensure there's a test that covers this removal + */ + if (offset.length === 1 && offset[0] === 0) { + offset[1] = 1; + } + /** + * Fill out if offset if fewer offsets than keyframes + */ + const remainder = length - valueKeyframes.length; + remainder > 0 && (0, _utils.fillOffset)(offset, remainder); + /** + * If only one value has been set, ie [1], push a null to the start of + * the keyframe array. This will let us mark a keyframe at this point + * that will later be hydrated with the previous value. + */ + valueKeyframes.length === 1 && valueKeyframes.unshift(null); + /** + * Add keyframes, mapping offsets to absolute time. + */ + (0, _editEs.addKeyframes)(valueSequence, valueKeyframes, easing, offset, startTime, targetTime); + maxDuration = Math.max(delay + duration, maxDuration); + totalDuration = Math.max(targetTime, totalDuration); + } + } + prevTime = currentTime; + currentTime += maxDuration; + } + /** + * For every element and value combination create a new animation. + */ + elementSequences.forEach((valueSequences, element) => { + for (const key in valueSequences) { + const valueSequence = valueSequences[key]; + /** + * Arrange all the keyframes in ascending time order. + */ + valueSequence.sort(_sortEs.compareByTime); + const keyframes = []; + const valueOffset = []; + const valueEasing = []; + /** + * For each keyframe, translate absolute times into + * relative offsets based on the total duration of the timeline. + */ + for (let i = 0; i < valueSequence.length; i++) { + const { + at, + value, + easing + } = valueSequence[i]; + keyframes.push(value); + valueOffset.push((0, _utils.progress)(0, totalDuration, at)); + valueEasing.push(easing || _utils.defaults.easing); + } + /** + * If the first keyframe doesn't land on offset: 0 + * provide one by duplicating the initial keyframe. This ensures + * it snaps to the first keyframe when the animation starts. + */ + if (valueOffset[0] !== 0) { + valueOffset.unshift(0); + keyframes.unshift(keyframes[0]); + valueEasing.unshift("linear"); + } + /** + * If the last keyframe doesn't land on offset: 1 + * provide one with a null wildcard value. This will ensure it + * stays static until the end of the animation. + */ + if (valueOffset[valueOffset.length - 1] !== 1) { + valueOffset.push(1); + keyframes.push(null); + } + animationDefinitions.push([element, key, keyframes, Object.assign(Object.assign(Object.assign({}, defaultOptions), { + duration: totalDuration, + easing: valueEasing, + offset: valueOffset + }), timelineOptions)]); + } + }); + return animationDefinitions; +} +function getElementSequence(element, sequences) { + !sequences.has(element) && sequences.set(element, {}); + return sequences.get(element); +} +function getValueSequence(name, sequences) { + if (!sequences[name]) sequences[name] = []; + return sequences[name]; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/timeline/utils/calc-time.es.js": +/*!********************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/timeline/utils/calc-time.es.js ***! + \********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.calcNextTime = calcNextTime; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +function calcNextTime(current, next, prev, labels) { + var _a; + if ((0, _utils.isNumber)(next)) { + return next; + } else if (next.startsWith("-") || next.startsWith("+")) { + return Math.max(0, current + parseFloat(next)); + } else if (next === "<") { + return prev; + } else { + return (_a = labels.get(next)) !== null && _a !== void 0 ? _a : current; + } +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/timeline/utils/edit.es.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/timeline/utils/edit.es.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.addKeyframes = addKeyframes; +exports.eraseKeyframes = eraseKeyframes; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +function eraseKeyframes(sequence, startTime, endTime) { + for (let i = 0; i < sequence.length; i++) { + const keyframe = sequence[i]; + if (keyframe.at > startTime && keyframe.at < endTime) { + (0, _utils.removeItem)(sequence, keyframe); + // If we remove this item we have to push the pointer back one + i--; + } + } +} +function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) { + /** + * Erase every existing value between currentTime and targetTime, + * this will essentially splice this timeline into any currently + * defined ones. + */ + eraseKeyframes(sequence, startTime, endTime); + for (let i = 0; i < keyframes.length; i++) { + sequence.push({ + value: keyframes[i], + at: (0, _utils.mix)(startTime, endTime, offset[i]), + easing: (0, _utils.getEasingForSegment)(easing, i) + }); + } +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/timeline/utils/sort.es.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/timeline/utils/sort.es.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.compareByTime = compareByTime; +function compareByTime(a, b) { + if (a.at === b.at) { + return a.value === null ? 1 : -1; + } else { + return a.at - b.at; + } +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/utils/resolve-elements.es.js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.resolveElements = resolveElements; +function resolveElements(elements, selectorCache) { + var _a; + if (typeof elements === "string") { + if (selectorCache) { + (_a = selectorCache[elements]) !== null && _a !== void 0 ? _a : selectorCache[elements] = document.querySelectorAll(elements); + elements = selectorCache[elements]; + } else { + elements = document.querySelectorAll(elements); + } + } else if (elements instanceof Element) { + elements = [elements]; + } + /** + * Return an empty array + */ + return Array.from(elements || []); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/dom/dist/utils/stagger.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/dom/dist/utils/stagger.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getFromIndex = getFromIndex; +exports.resolveOption = resolveOption; +exports.stagger = stagger; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _animation = __webpack_require__(/*! @motionone/animation */ "../../../node_modules/@motionone/animation/dist/index.es.js"); +function stagger() { + let duration = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0.1; + let { + start = 0, + from = 0, + easing + } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return (i, total) => { + const fromIndex = (0, _utils.isNumber)(from) ? from : getFromIndex(from, total); + const distance = Math.abs(fromIndex - i); + let delay = duration * distance; + if (easing) { + const maxDelay = total * duration; + const easingFunction = (0, _animation.getEasingFunction)(easing); + delay = easingFunction(delay / maxDelay) * maxDelay; + } + return start + delay; + }; +} +function getFromIndex(from, total) { + if (from === "first") { + return 0; + } else { + const lastIndex = total - 1; + return from === "last" ? lastIndex : lastIndex / 2; + } +} +function resolveOption(option, i, total) { + return typeof option === "function" ? option(i, total) : option; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/easing/dist/cubic-bezier.es.js": +/*!***********************************************************************!*\ + !*** ../../../node_modules/@motionone/easing/dist/cubic-bezier.es.js ***! + \***********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.cubicBezier = cubicBezier; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +/* + Bezier function generator + + This has been modified from Gaëtan Renaudeau's BezierEasing + https://github.com/gre/bezier-easing/blob/master/src/index.js + https://github.com/gre/bezier-easing/blob/master/LICENSE + + I've removed the newtonRaphsonIterate algo because in benchmarking it + wasn't noticiably faster than binarySubdivision, indeed removing it + usually improved times, depending on the curve. + + I also removed the lookup table, as for the added bundle size and loop we're + only cutting ~4 or so subdivision iterations. I bumped the max iterations up + to 12 to compensate and this still tended to be faster for no perceivable + loss in accuracy. + + Usage + const easeOut = cubicBezier(.17,.67,.83,.67); + const x = easeOut(0.5); // returns 0.627... +*/ +// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. +const calcBezier = (t, a1, a2) => (((1.0 - 3.0 * a2 + 3.0 * a1) * t + (3.0 * a2 - 6.0 * a1)) * t + 3.0 * a1) * t; +const subdivisionPrecision = 0.0000001; +const subdivisionMaxIterations = 12; +function binarySubdivide(x, lowerBound, upperBound, mX1, mX2) { + let currentX; + let currentT; + let i = 0; + do { + currentT = lowerBound + (upperBound - lowerBound) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - x; + if (currentX > 0.0) { + upperBound = currentT; + } else { + lowerBound = currentT; + } + } while (Math.abs(currentX) > subdivisionPrecision && ++i < subdivisionMaxIterations); + return currentT; +} +function cubicBezier(mX1, mY1, mX2, mY2) { + // If this is a linear gradient, return linear easing + if (mX1 === mY1 && mX2 === mY2) return _utils.noopReturn; + const getTForX = aX => binarySubdivide(aX, 0, 1, mX1, mX2); + // If animation is at start/end, return t without easing + return t => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/easing/dist/index.es.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@motionone/easing/dist/index.es.js ***! + \****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "cubicBezier", ({ + enumerable: true, + get: function () { + return _cubicBezierEs.cubicBezier; + } +})); +Object.defineProperty(exports, "steps", ({ + enumerable: true, + get: function () { + return _stepsEs.steps; + } +})); +var _cubicBezierEs = __webpack_require__(/*! ./cubic-bezier.es.js */ "../../../node_modules/@motionone/easing/dist/cubic-bezier.es.js"); +var _stepsEs = __webpack_require__(/*! ./steps.es.js */ "../../../node_modules/@motionone/easing/dist/steps.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/easing/dist/steps.es.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@motionone/easing/dist/steps.es.js ***! + \****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.steps = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const steps = function (steps) { + let direction = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "end"; + return progress => { + progress = direction === "end" ? Math.min(progress, 0.999) : Math.max(progress, 0.001); + const expanded = progress * steps; + const rounded = direction === "end" ? Math.floor(expanded) : Math.ceil(expanded); + return (0, _utils.clamp)(0, 1, rounded / steps); + }; +}; +exports.steps = steps; + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/glide/index.es.js": +/*!**************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/glide/index.es.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.glide = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _velocityEs = __webpack_require__(/*! ../utils/velocity.es.js */ "../../../node_modules/@motionone/generators/dist/utils/velocity.es.js"); +var _indexEs = __webpack_require__(/*! ../spring/index.es.js */ "../../../node_modules/@motionone/generators/dist/spring/index.es.js"); +const glide = _ref => { + let { + from = 0, + velocity = 0.0, + power = 0.8, + decay = 0.325, + bounceDamping, + bounceStiffness, + changeTarget, + min, + max, + restDistance = 0.5, + restSpeed + } = _ref; + decay = _utils.time.ms(decay); + const state = { + hasReachedTarget: false, + done: false, + current: from, + target: from + }; + const isOutOfBounds = v => min !== undefined && v < min || max !== undefined && v > max; + const nearestBoundary = v => { + if (min === undefined) return max; + if (max === undefined) return min; + return Math.abs(min - v) < Math.abs(max - v) ? min : max; + }; + let amplitude = power * velocity; + const ideal = from + amplitude; + const target = changeTarget === undefined ? ideal : changeTarget(ideal); + state.target = target; + /** + * If the target has changed we need to re-calculate the amplitude, otherwise + * the animation will start from the wrong position. + */ + if (target !== ideal) amplitude = target - from; + const calcDelta = t => -amplitude * Math.exp(-t / decay); + const calcLatest = t => target + calcDelta(t); + const applyFriction = t => { + const delta = calcDelta(t); + const latest = calcLatest(t); + state.done = Math.abs(delta) <= restDistance; + state.current = state.done ? target : latest; + }; + /** + * Ideally this would resolve for t in a stateless way, we could + * do that by always precalculating the animation but as we know + * this will be done anyway we can assume that spring will + * be discovered during that. + */ + let timeReachedBoundary; + let spring$1; + const checkCatchBoundary = t => { + if (!isOutOfBounds(state.current)) return; + timeReachedBoundary = t; + spring$1 = (0, _indexEs.spring)({ + from: state.current, + to: nearestBoundary(state.current), + velocity: (0, _velocityEs.calcGeneratorVelocity)(calcLatest, t, state.current), + damping: bounceDamping, + stiffness: bounceStiffness, + restDistance, + restSpeed + }); + }; + checkCatchBoundary(0); + return t => { + /** + * We need to resolve the friction to figure out if we need a + * spring but we don't want to do this twice per frame. So here + * we flag if we updated for this frame and later if we did + * we can skip doing it again. + */ + let hasUpdatedFrame = false; + if (!spring$1 && timeReachedBoundary === undefined) { + hasUpdatedFrame = true; + applyFriction(t); + checkCatchBoundary(t); + } + /** + * If we have a spring and the provided t is beyond the moment the friction + * animation crossed the min/max boundary, use the spring. + */ + if (timeReachedBoundary !== undefined && t > timeReachedBoundary) { + state.hasReachedTarget = true; + return spring$1(t - timeReachedBoundary); + } else { + state.hasReachedTarget = false; + !hasUpdatedFrame && applyFriction(t); + return state; + } + }; +}; +exports.glide = glide; + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/index.es.js": +/*!********************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/index.es.js ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "calcGeneratorVelocity", ({ + enumerable: true, + get: function () { + return _velocityEs.calcGeneratorVelocity; + } +})); +Object.defineProperty(exports, "glide", ({ + enumerable: true, + get: function () { + return _indexEs.glide; + } +})); +Object.defineProperty(exports, "pregenerateKeyframes", ({ + enumerable: true, + get: function () { + return _pregenerateKeyframesEs.pregenerateKeyframes; + } +})); +Object.defineProperty(exports, "spring", ({ + enumerable: true, + get: function () { + return _indexEs2.spring; + } +})); +var _indexEs = __webpack_require__(/*! ./glide/index.es.js */ "../../../node_modules/@motionone/generators/dist/glide/index.es.js"); +var _indexEs2 = __webpack_require__(/*! ./spring/index.es.js */ "../../../node_modules/@motionone/generators/dist/spring/index.es.js"); +var _pregenerateKeyframesEs = __webpack_require__(/*! ./utils/pregenerate-keyframes.es.js */ "../../../node_modules/@motionone/generators/dist/utils/pregenerate-keyframes.es.js"); +var _velocityEs = __webpack_require__(/*! ./utils/velocity.es.js */ "../../../node_modules/@motionone/generators/dist/utils/velocity.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/spring/defaults.es.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/spring/defaults.es.js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.defaults = void 0; +const defaults = { + stiffness: 100.0, + damping: 10.0, + mass: 1.0 +}; +exports.defaults = defaults; + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/spring/index.es.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/spring/index.es.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.spring = void 0; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +var _defaultsEs = __webpack_require__(/*! ./defaults.es.js */ "../../../node_modules/@motionone/generators/dist/spring/defaults.es.js"); +var _utilsEs = __webpack_require__(/*! ./utils.es.js */ "../../../node_modules/@motionone/generators/dist/spring/utils.es.js"); +var _hasReachedTargetEs = __webpack_require__(/*! ../utils/has-reached-target.es.js */ "../../../node_modules/@motionone/generators/dist/utils/has-reached-target.es.js"); +var _velocityEs = __webpack_require__(/*! ../utils/velocity.es.js */ "../../../node_modules/@motionone/generators/dist/utils/velocity.es.js"); +const spring = function () { + let { + stiffness = _defaultsEs.defaults.stiffness, + damping = _defaultsEs.defaults.damping, + mass = _defaultsEs.defaults.mass, + from = 0, + to = 1, + velocity = 0.0, + restSpeed = 2, + restDistance = 0.5 + } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + velocity = velocity ? _utils.time.s(velocity) : 0.0; + const state = { + done: false, + hasReachedTarget: false, + current: from, + target: to + }; + const initialDelta = to - from; + const undampedAngularFreq = Math.sqrt(stiffness / mass) / 1000; + const dampingRatio = (0, _utilsEs.calcDampingRatio)(stiffness, damping, mass); + let resolveSpring; + if (dampingRatio < 1) { + const angularFreq = undampedAngularFreq * Math.sqrt(1 - dampingRatio * dampingRatio); + // Underdamped spring (bouncy) + resolveSpring = t => to - Math.exp(-dampingRatio * undampedAngularFreq * t) * ((-velocity + dampingRatio * undampedAngularFreq * initialDelta) / angularFreq * Math.sin(angularFreq * t) + initialDelta * Math.cos(angularFreq * t)); + } else { + // Critically damped spring + resolveSpring = t => { + return to - Math.exp(-undampedAngularFreq * t) * (initialDelta + (-velocity + undampedAngularFreq * initialDelta) * t); + }; + } + return t => { + state.current = resolveSpring(t); + const currentVelocity = t === 0 ? velocity : (0, _velocityEs.calcGeneratorVelocity)(resolveSpring, t, state.current); + const isBelowVelocityThreshold = Math.abs(currentVelocity) <= restSpeed; + const isBelowDisplacementThreshold = Math.abs(to - state.current) <= restDistance; + state.done = isBelowVelocityThreshold && isBelowDisplacementThreshold; + state.hasReachedTarget = (0, _hasReachedTargetEs.hasReachedTarget)(from, to, state.current); + return state; + }; +}; +exports.spring = spring; + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/spring/utils.es.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/spring/utils.es.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.calcDampingRatio = void 0; +var _defaultsEs = __webpack_require__(/*! ./defaults.es.js */ "../../../node_modules/@motionone/generators/dist/spring/defaults.es.js"); +const calcDampingRatio = function () { + let stiffness = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _defaultsEs.defaults.stiffness; + let damping = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _defaultsEs.defaults.damping; + let mass = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _defaultsEs.defaults.mass; + return damping / (2 * Math.sqrt(stiffness * mass)); +}; +exports.calcDampingRatio = calcDampingRatio; + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/utils/has-reached-target.es.js": +/*!***************************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/utils/has-reached-target.es.js ***! + \***************************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.hasReachedTarget = hasReachedTarget; +function hasReachedTarget(origin, target, current) { + return origin < target && current >= target || origin > target && current <= target; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/utils/pregenerate-keyframes.es.js": +/*!******************************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/utils/pregenerate-keyframes.es.js ***! + \******************************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.pregenerateKeyframes = pregenerateKeyframes; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const timeStep = 10; +const maxDuration = 10000; +function pregenerateKeyframes(generator) { + let toUnit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _utils.noopReturn; + let overshootDuration = undefined; + let timestamp = timeStep; + let state = generator(0); + const keyframes = [toUnit(state.current)]; + while (!state.done && timestamp < maxDuration) { + state = generator(timestamp); + keyframes.push(toUnit(state.done ? state.target : state.current)); + if (overshootDuration === undefined && state.hasReachedTarget) { + overshootDuration = timestamp; + } + timestamp += timeStep; + } + const duration = timestamp - timeStep; + /** + * If generating an animation that didn't actually move, + * generate a second keyframe so we have an origin and target. + */ + if (keyframes.length === 1) keyframes.push(state.current); + return { + keyframes, + duration: duration / 1000, + overshootDuration: (overshootDuration !== null && overshootDuration !== void 0 ? overshootDuration : duration) / 1000 + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/generators/dist/utils/velocity.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/generators/dist/utils/velocity.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.calcGeneratorVelocity = calcGeneratorVelocity; +var _utils = __webpack_require__(/*! @motionone/utils */ "../../../node_modules/@motionone/utils/dist/index.es.js"); +const sampleT = 5; // ms +function calcGeneratorVelocity(resolveValue, t, current) { + const prevT = Math.max(t - sampleT, 0); + return (0, _utils.velocityPerSecond)(current - resolveValue(prevT), t - prevT); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/types/dist/MotionValue.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/types/dist/MotionValue.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MotionValue = void 0; +/** + * The MotionValue tracks the state of a single animatable + * value. Currently, updatedAt and current are unused. The + * long term idea is to use this to minimise the number + * of DOM reads, and to abstract the DOM interactions here. + */ +class MotionValue { + setAnimation(animation) { + this.animation = animation; + animation === null || animation === void 0 ? void 0 : animation.finished.then(() => this.clearAnimation()).catch(() => {}); + } + clearAnimation() { + this.animation = this.generator = undefined; + } +} +exports.MotionValue = MotionValue; + +/***/ }), + +/***/ "../../../node_modules/@motionone/types/dist/index.es.js": +/*!***************************************************************!*\ + !*** ../../../node_modules/@motionone/types/dist/index.es.js ***! + \***************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "MotionValue", ({ + enumerable: true, + get: function () { + return _MotionValueEs.MotionValue; + } +})); +var _MotionValueEs = __webpack_require__(/*! ./MotionValue.es.js */ "../../../node_modules/@motionone/types/dist/MotionValue.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/array.es.js": +/*!***************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/array.es.js ***! + \***************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.addUniqueItem = addUniqueItem; +exports.removeItem = removeItem; +function addUniqueItem(array, item) { + array.indexOf(item) === -1 && array.push(item); +} +function removeItem(arr, item) { + const index = arr.indexOf(item); + index > -1 && arr.splice(index, 1); +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/clamp.es.js": +/*!***************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/clamp.es.js ***! + \***************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.clamp = void 0; +const clamp = (min, max, v) => Math.min(Math.max(v, min), max); +exports.clamp = clamp; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/defaults.es.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/defaults.es.js ***! + \******************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.defaults = void 0; +const defaults = { + duration: 0.3, + delay: 0, + endDelay: 0, + repeat: 0, + easing: "ease" +}; +exports.defaults = defaults; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/easing.es.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/easing.es.js ***! + \****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getEasingForSegment = getEasingForSegment; +var _isEasingListEs = __webpack_require__(/*! ./is-easing-list.es.js */ "../../../node_modules/@motionone/utils/dist/is-easing-list.es.js"); +var _wrapEs = __webpack_require__(/*! ./wrap.es.js */ "../../../node_modules/@motionone/utils/dist/wrap.es.js"); +function getEasingForSegment(easing, i) { + return (0, _isEasingListEs.isEasingList)(easing) ? easing[(0, _wrapEs.wrap)(0, easing.length, i)] : easing; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/index.es.js": +/*!***************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/index.es.js ***! + \***************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "addUniqueItem", ({ + enumerable: true, + get: function () { + return _arrayEs.addUniqueItem; + } +})); +Object.defineProperty(exports, "clamp", ({ + enumerable: true, + get: function () { + return _clampEs.clamp; + } +})); +Object.defineProperty(exports, "defaultOffset", ({ + enumerable: true, + get: function () { + return _offsetEs.defaultOffset; + } +})); +Object.defineProperty(exports, "defaults", ({ + enumerable: true, + get: function () { + return _defaultsEs.defaults; + } +})); +Object.defineProperty(exports, "fillOffset", ({ + enumerable: true, + get: function () { + return _offsetEs.fillOffset; + } +})); +Object.defineProperty(exports, "getEasingForSegment", ({ + enumerable: true, + get: function () { + return _easingEs.getEasingForSegment; + } +})); +Object.defineProperty(exports, "interpolate", ({ + enumerable: true, + get: function () { + return _interpolateEs.interpolate; + } +})); +Object.defineProperty(exports, "isCubicBezier", ({ + enumerable: true, + get: function () { + return _isCubicBezierEs.isCubicBezier; + } +})); +Object.defineProperty(exports, "isEasingGenerator", ({ + enumerable: true, + get: function () { + return _isEasingGeneratorEs.isEasingGenerator; + } +})); +Object.defineProperty(exports, "isEasingList", ({ + enumerable: true, + get: function () { + return _isEasingListEs.isEasingList; + } +})); +Object.defineProperty(exports, "isFunction", ({ + enumerable: true, + get: function () { + return _isFunctionEs.isFunction; + } +})); +Object.defineProperty(exports, "isNumber", ({ + enumerable: true, + get: function () { + return _isNumberEs.isNumber; + } +})); +Object.defineProperty(exports, "isString", ({ + enumerable: true, + get: function () { + return _isStringEs.isString; + } +})); +Object.defineProperty(exports, "mix", ({ + enumerable: true, + get: function () { + return _mixEs.mix; + } +})); +Object.defineProperty(exports, "noop", ({ + enumerable: true, + get: function () { + return _noopEs.noop; + } +})); +Object.defineProperty(exports, "noopReturn", ({ + enumerable: true, + get: function () { + return _noopEs.noopReturn; + } +})); +Object.defineProperty(exports, "progress", ({ + enumerable: true, + get: function () { + return _progressEs.progress; + } +})); +Object.defineProperty(exports, "removeItem", ({ + enumerable: true, + get: function () { + return _arrayEs.removeItem; + } +})); +Object.defineProperty(exports, "time", ({ + enumerable: true, + get: function () { + return _timeEs.time; + } +})); +Object.defineProperty(exports, "velocityPerSecond", ({ + enumerable: true, + get: function () { + return _velocityEs.velocityPerSecond; + } +})); +Object.defineProperty(exports, "wrap", ({ + enumerable: true, + get: function () { + return _wrapEs.wrap; + } +})); +var _arrayEs = __webpack_require__(/*! ./array.es.js */ "../../../node_modules/@motionone/utils/dist/array.es.js"); +var _clampEs = __webpack_require__(/*! ./clamp.es.js */ "../../../node_modules/@motionone/utils/dist/clamp.es.js"); +var _defaultsEs = __webpack_require__(/*! ./defaults.es.js */ "../../../node_modules/@motionone/utils/dist/defaults.es.js"); +var _easingEs = __webpack_require__(/*! ./easing.es.js */ "../../../node_modules/@motionone/utils/dist/easing.es.js"); +var _interpolateEs = __webpack_require__(/*! ./interpolate.es.js */ "../../../node_modules/@motionone/utils/dist/interpolate.es.js"); +var _isCubicBezierEs = __webpack_require__(/*! ./is-cubic-bezier.es.js */ "../../../node_modules/@motionone/utils/dist/is-cubic-bezier.es.js"); +var _isEasingGeneratorEs = __webpack_require__(/*! ./is-easing-generator.es.js */ "../../../node_modules/@motionone/utils/dist/is-easing-generator.es.js"); +var _isEasingListEs = __webpack_require__(/*! ./is-easing-list.es.js */ "../../../node_modules/@motionone/utils/dist/is-easing-list.es.js"); +var _isFunctionEs = __webpack_require__(/*! ./is-function.es.js */ "../../../node_modules/@motionone/utils/dist/is-function.es.js"); +var _isNumberEs = __webpack_require__(/*! ./is-number.es.js */ "../../../node_modules/@motionone/utils/dist/is-number.es.js"); +var _isStringEs = __webpack_require__(/*! ./is-string.es.js */ "../../../node_modules/@motionone/utils/dist/is-string.es.js"); +var _mixEs = __webpack_require__(/*! ./mix.es.js */ "../../../node_modules/@motionone/utils/dist/mix.es.js"); +var _noopEs = __webpack_require__(/*! ./noop.es.js */ "../../../node_modules/@motionone/utils/dist/noop.es.js"); +var _offsetEs = __webpack_require__(/*! ./offset.es.js */ "../../../node_modules/@motionone/utils/dist/offset.es.js"); +var _progressEs = __webpack_require__(/*! ./progress.es.js */ "../../../node_modules/@motionone/utils/dist/progress.es.js"); +var _timeEs = __webpack_require__(/*! ./time.es.js */ "../../../node_modules/@motionone/utils/dist/time.es.js"); +var _velocityEs = __webpack_require__(/*! ./velocity.es.js */ "../../../node_modules/@motionone/utils/dist/velocity.es.js"); +var _wrapEs = __webpack_require__(/*! ./wrap.es.js */ "../../../node_modules/@motionone/utils/dist/wrap.es.js"); + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/interpolate.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/interpolate.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.interpolate = interpolate; +var _mixEs = __webpack_require__(/*! ./mix.es.js */ "../../../node_modules/@motionone/utils/dist/mix.es.js"); +var _noopEs = __webpack_require__(/*! ./noop.es.js */ "../../../node_modules/@motionone/utils/dist/noop.es.js"); +var _offsetEs = __webpack_require__(/*! ./offset.es.js */ "../../../node_modules/@motionone/utils/dist/offset.es.js"); +var _progressEs = __webpack_require__(/*! ./progress.es.js */ "../../../node_modules/@motionone/utils/dist/progress.es.js"); +var _easingEs = __webpack_require__(/*! ./easing.es.js */ "../../../node_modules/@motionone/utils/dist/easing.es.js"); +var _clampEs = __webpack_require__(/*! ./clamp.es.js */ "../../../node_modules/@motionone/utils/dist/clamp.es.js"); +function interpolate(output) { + let input = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : (0, _offsetEs.defaultOffset)(output.length); + let easing = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _noopEs.noopReturn; + const length = output.length; + /** + * If the input length is lower than the output we + * fill the input to match. This currently assumes the input + * is an animation progress value so is a good candidate for + * moving outside the function. + */ + const remainder = length - input.length; + remainder > 0 && (0, _offsetEs.fillOffset)(input, remainder); + return t => { + let i = 0; + for (; i < length - 2; i++) { + if (t < input[i + 1]) break; + } + let progressInRange = (0, _clampEs.clamp)(0, 1, (0, _progressEs.progress)(input[i], input[i + 1], t)); + const segmentEasing = (0, _easingEs.getEasingForSegment)(easing, i); + progressInRange = segmentEasing(progressInRange); + return (0, _mixEs.mix)(output[i], output[i + 1], progressInRange); + }; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-cubic-bezier.es.js": +/*!*************************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-cubic-bezier.es.js ***! + \*************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isCubicBezier = void 0; +var _isNumberEs = __webpack_require__(/*! ./is-number.es.js */ "../../../node_modules/@motionone/utils/dist/is-number.es.js"); +const isCubicBezier = easing => Array.isArray(easing) && (0, _isNumberEs.isNumber)(easing[0]); +exports.isCubicBezier = isCubicBezier; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-easing-generator.es.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-easing-generator.es.js ***! + \*****************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isEasingGenerator = void 0; +const isEasingGenerator = easing => typeof easing === "object" && Boolean(easing.createAnimation); +exports.isEasingGenerator = isEasingGenerator; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-easing-list.es.js": +/*!************************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-easing-list.es.js ***! + \************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isEasingList = void 0; +var _isNumberEs = __webpack_require__(/*! ./is-number.es.js */ "../../../node_modules/@motionone/utils/dist/is-number.es.js"); +const isEasingList = easing => Array.isArray(easing) && !(0, _isNumberEs.isNumber)(easing[0]); +exports.isEasingList = isEasingList; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-function.es.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-function.es.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isFunction = void 0; +const isFunction = value => typeof value === "function"; +exports.isFunction = isFunction; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-number.es.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-number.es.js ***! + \*******************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isNumber = void 0; +const isNumber = value => typeof value === "number"; +exports.isNumber = isNumber; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/is-string.es.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/is-string.es.js ***! + \*******************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isString = void 0; +const isString = value => typeof value === "string"; +exports.isString = isString; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/mix.es.js": +/*!*************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/mix.es.js ***! + \*************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.mix = void 0; +const mix = (min, max, progress) => -progress * min + progress * max + min; +exports.mix = mix; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/noop.es.js": +/*!**************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/noop.es.js ***! + \**************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.noopReturn = exports.noop = void 0; +const noop = () => {}; +exports.noop = noop; +const noopReturn = v => v; +exports.noopReturn = noopReturn; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/offset.es.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/offset.es.js ***! + \****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.defaultOffset = defaultOffset; +exports.fillOffset = fillOffset; +var _mixEs = __webpack_require__(/*! ./mix.es.js */ "../../../node_modules/@motionone/utils/dist/mix.es.js"); +var _progressEs = __webpack_require__(/*! ./progress.es.js */ "../../../node_modules/@motionone/utils/dist/progress.es.js"); +function fillOffset(offset, remaining) { + const min = offset[offset.length - 1]; + for (let i = 1; i <= remaining; i++) { + const offsetProgress = (0, _progressEs.progress)(0, remaining, i); + offset.push((0, _mixEs.mix)(min, 1, offsetProgress)); + } +} +function defaultOffset(length) { + const offset = [0]; + fillOffset(offset, length - 1); + return offset; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/progress.es.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/progress.es.js ***! + \******************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.progress = void 0; +const progress = (min, max, value) => max - min === 0 ? 1 : (value - min) / (max - min); +exports.progress = progress; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/time.es.js": +/*!**************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/time.es.js ***! + \**************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.time = void 0; +const time = { + ms: seconds => seconds * 1000, + s: milliseconds => milliseconds / 1000 +}; +exports.time = time; + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/velocity.es.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/velocity.es.js ***! + \******************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.velocityPerSecond = velocityPerSecond; +/* + Convert velocity into velocity per second + + @param [number]: Unit per frame + @param [number]: Frame duration in ms +*/ +function velocityPerSecond(velocity, frameDuration) { + return frameDuration ? velocity * (1000 / frameDuration) : 0; +} + +/***/ }), + +/***/ "../../../node_modules/@motionone/utils/dist/wrap.es.js": +/*!**************************************************************!*\ + !*** ../../../node_modules/@motionone/utils/dist/wrap.es.js ***! + \**************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.wrap = void 0; +const wrap = (min, max, v) => { + const rangeSize = max - min; + return ((v - min) % rangeSize + rangeSize) % rangeSize + min; +}; +exports.wrap = wrap; + +/***/ }), + +/***/ "../../../node_modules/@n1ru4l/push-pull-async-iterable-iterator/index.js": +/*!********************************************************************************!*\ + !*** ../../../node_modules/@n1ru4l/push-pull-async-iterable-iterator/index.js ***! + \********************************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +function createDeferred() { + const d = {}; + d.promise = new Promise((resolve, reject) => { + d.resolve = resolve; + d.reject = reject; + }); + return d; +} +const SYMBOL_FINISHED = Symbol(); +const SYMBOL_NEW_VALUE = Symbol(); +/** + * makePushPullAsyncIterableIterator + * + * The iterable will publish values until return or throw is called. + * Afterwards it is in the completed state and cannot be used for publishing any further values. + * It will handle back-pressure and keep pushed values until they are consumed by a source. + */ +function makePushPullAsyncIterableIterator() { + let isRunning = true; + const values = []; + let newValueD = createDeferred(); + const finishedD = createDeferred(); + const asyncIterableIterator = async function* PushPullAsyncIterableIterator() { + while (true) { + if (values.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + yield values.shift(); + } else { + const result = await Promise.race([newValueD.promise, finishedD.promise]); + if (result === SYMBOL_FINISHED) { + break; + } + if (result !== SYMBOL_NEW_VALUE) { + throw result; + } + } + } + }(); + function pushValue(value) { + if (isRunning === false) { + // TODO: Should this throw? + return; + } + values.push(value); + newValueD.resolve(SYMBOL_NEW_VALUE); + newValueD = createDeferred(); + } + // We monkey patch the original generator for clean-up + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const originalReturn = asyncIterableIterator.return.bind(asyncIterableIterator); + asyncIterableIterator.return = function () { + isRunning = false; + finishedD.resolve(SYMBOL_FINISHED); + return originalReturn(...arguments); + }; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const originalThrow = asyncIterableIterator.throw.bind(asyncIterableIterator); + asyncIterableIterator.throw = err => { + isRunning = false; + finishedD.resolve(err); + return originalThrow(err); + }; + return { + pushValue, + asyncIterableIterator + }; +} +const makeAsyncIterableIteratorFromSink = make => { + const { + pushValue, + asyncIterableIterator + } = makePushPullAsyncIterableIterator(); + const dispose = make({ + next: value => { + pushValue(value); + }, + complete: () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + asyncIterableIterator.return(); + }, + error: err => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + asyncIterableIterator.throw(err); + } + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const originalReturn = asyncIterableIterator.return; + let returnValue = undefined; + asyncIterableIterator.return = () => { + if (returnValue === undefined) { + dispose(); + returnValue = originalReturn(); + } + return returnValue; + }; + return asyncIterableIterator; +}; +function applyAsyncIterableIteratorToSink(asyncIterableIterator, sink) { + const run = async () => { + try { + for await (const value of asyncIterableIterator) { + sink.next(value); + } + sink.complete(); + } catch (err) { + sink.error(err); + } + }; + run(); + return () => { + var _a; + (_a = asyncIterableIterator.return) === null || _a === void 0 ? void 0 : _a.call(asyncIterableIterator); + }; +} +function isAsyncIterable(input) { + return typeof input === "object" && input !== null && ( + // The AsyncGenerator check is for Safari on iOS which currently does not have + // Symbol.asyncIterator implemented + // That means every custom AsyncIterable must be built using a AsyncGeneratorFunction (async function * () {}) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input[Symbol.toStringTag] === "AsyncGenerator" || Symbol.asyncIterator && Symbol.asyncIterator in input); +} +exports.applyAsyncIterableIteratorToSink = applyAsyncIterableIteratorToSink; +exports.isAsyncIterable = isAsyncIterable; +exports.makeAsyncIterableIteratorFromSink = makeAsyncIterableIteratorFromSink; +exports.makePushPullAsyncIterableIterator = makePushPullAsyncIterableIterator; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/primitive/dist/index.js": +/*!***************************************************************!*\ + !*** ../../../node_modules/@radix-ui/primitive/dist/index.js ***! + \***************************************************************/ +/***/ (function(module) { + + + +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "composeEventHandlers", () => $1a6a90a521dcd173$export$b9ecd428b558ff10); +function $1a6a90a521dcd173$export$b9ecd428b558ff10(originalEventHandler, ourEventHandler) { + let { + checkForDefaultPrevented = true + } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return function handleEvent(event) { + originalEventHandler === null || originalEventHandler === void 0 || originalEventHandler(event); + if (checkForDefaultPrevented === false || !event.defaultPrevented) return ourEventHandler === null || ourEventHandler === void 0 ? void 0 : ourEventHandler(event); + }; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-arrow/dist/index.js": +/*!*****************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-arrow/dist/index.js ***! + \*****************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $eQpDd$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $eQpDd$react = __webpack_require__(/*! react */ "react"); +var $eQpDd$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "Arrow", () => $09f4ad68a9251bc3$export$21b07c8f274aebd5); +$parcel$export(module.exports, "Root", () => $09f4ad68a9251bc3$export$be92b6f5f03c0fe9); + +/* ------------------------------------------------------------------------------------------------- + * Arrow + * -----------------------------------------------------------------------------------------------*/ +const $09f4ad68a9251bc3$var$NAME = 'Arrow'; +const $09f4ad68a9251bc3$export$21b07c8f274aebd5 = /*#__PURE__*/$eQpDd$react.forwardRef((props, forwardedRef) => { + const { + children: children, + width = 10, + height = 5, + ...arrowProps + } = props; + return /*#__PURE__*/$eQpDd$react.createElement($eQpDd$radixuireactprimitive.Primitive.svg, $parcel$interopDefault($eQpDd$babelruntimehelpersextends)({}, arrowProps, { + ref: forwardedRef, + width: width, + height: height, + viewBox: "0 0 30 10", + preserveAspectRatio: "none" + }), props.asChild ? children : /*#__PURE__*/$eQpDd$react.createElement("polygon", { + points: "0,0 30,0 15,10" + })); +}); +/*#__PURE__*/ +Object.assign($09f4ad68a9251bc3$export$21b07c8f274aebd5, { + displayName: $09f4ad68a9251bc3$var$NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $09f4ad68a9251bc3$export$be92b6f5f03c0fe9 = $09f4ad68a9251bc3$export$21b07c8f274aebd5; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-collection/dist/index.js": +/*!**********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-collection/dist/index.js ***! + \**********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $hnlpS$react = __webpack_require__(/*! react */ "react"); +var $hnlpS$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $hnlpS$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $hnlpS$radixuireactslot = __webpack_require__(/*! @radix-ui/react-slot */ "../../../node_modules/@radix-ui/react-slot/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createCollection", () => $1a96635ec239608b$export$c74125a8e3af6bb2); + +// We have resorted to returning slots directly rather than exposing primitives that can then +// be slotted like ``. +// This is because we encountered issues with generic types that cannot be statically analysed +// due to creating them dynamically via createCollection. +function $1a96635ec239608b$export$c74125a8e3af6bb2(name) { + /* ----------------------------------------------------------------------------------------------- + * CollectionProvider + * ---------------------------------------------------------------------------------------------*/ + const PROVIDER_NAME = name + 'CollectionProvider'; + const [createCollectionContext, createCollectionScope] = $hnlpS$radixuireactcontext.createContextScope(PROVIDER_NAME); + const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(PROVIDER_NAME, { + collectionRef: { + current: null + }, + itemMap: new Map() + }); + const CollectionProvider = props => { + const { + scope: scope, + children: children + } = props; + const ref = $parcel$interopDefault($hnlpS$react).useRef(null); + const itemMap = $parcel$interopDefault($hnlpS$react).useRef(new Map()).current; + return /*#__PURE__*/$parcel$interopDefault($hnlpS$react).createElement(CollectionProviderImpl, { + scope: scope, + itemMap: itemMap, + collectionRef: ref + }, children); + }; + /*#__PURE__*/ + Object.assign(CollectionProvider, { + displayName: PROVIDER_NAME + }); + /* ----------------------------------------------------------------------------------------------- + * CollectionSlot + * ---------------------------------------------------------------------------------------------*/ + const COLLECTION_SLOT_NAME = name + 'CollectionSlot'; + const CollectionSlot = /*#__PURE__*/$parcel$interopDefault($hnlpS$react).forwardRef((props, forwardedRef) => { + const { + scope: scope, + children: children + } = props; + const context = useCollectionContext(COLLECTION_SLOT_NAME, scope); + const composedRefs = $hnlpS$radixuireactcomposerefs.useComposedRefs(forwardedRef, context.collectionRef); + return /*#__PURE__*/$parcel$interopDefault($hnlpS$react).createElement($hnlpS$radixuireactslot.Slot, { + ref: composedRefs + }, children); + }); + /*#__PURE__*/ + Object.assign(CollectionSlot, { + displayName: COLLECTION_SLOT_NAME + }); + /* ----------------------------------------------------------------------------------------------- + * CollectionItem + * ---------------------------------------------------------------------------------------------*/ + const ITEM_SLOT_NAME = name + 'CollectionItemSlot'; + const ITEM_DATA_ATTR = 'data-radix-collection-item'; + const CollectionItemSlot = /*#__PURE__*/$parcel$interopDefault($hnlpS$react).forwardRef((props, forwardedRef) => { + const { + scope: scope, + children: children, + ...itemData + } = props; + const ref = $parcel$interopDefault($hnlpS$react).useRef(null); + const composedRefs = $hnlpS$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + const context = useCollectionContext(ITEM_SLOT_NAME, scope); + $parcel$interopDefault($hnlpS$react).useEffect(() => { + context.itemMap.set(ref, { + ref: ref, + ...itemData + }); + return () => void context.itemMap.delete(ref); + }); + return /*#__PURE__*/$parcel$interopDefault($hnlpS$react).createElement($hnlpS$radixuireactslot.Slot, { + [ITEM_DATA_ATTR]: '', + ref: composedRefs + }, children); + }); + /*#__PURE__*/ + Object.assign(CollectionItemSlot, { + displayName: ITEM_SLOT_NAME + }); + /* ----------------------------------------------------------------------------------------------- + * useCollection + * ---------------------------------------------------------------------------------------------*/ + function useCollection(scope) { + const context = useCollectionContext(name + 'CollectionConsumer', scope); + const getItems = $parcel$interopDefault($hnlpS$react).useCallback(() => { + const collectionNode = context.collectionRef.current; + if (!collectionNode) return []; + const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`)); + const items = Array.from(context.itemMap.values()); + const orderedItems = items.sort((a, b) => orderedNodes.indexOf(a.ref.current) - orderedNodes.indexOf(b.ref.current)); + return orderedItems; + }, [context.collectionRef, context.itemMap]); + return getItems; + } + return [{ + Provider: CollectionProvider, + Slot: CollectionSlot, + ItemSlot: CollectionItemSlot + }, useCollection, createCollectionScope]; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js": +/*!************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-compose-refs/dist/index.js ***! + \************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $dJwbH$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "composeRefs", () => $9c2aaba23466b352$export$43e446d32b3d21af); +$parcel$export(module.exports, "useComposedRefs", () => $9c2aaba23466b352$export$c7b2cbe3552a0d05); + +/** + * Set a given ref to a given value + * This utility takes care of different types of refs: callback refs and RefObject(s) + */ +function $9c2aaba23466b352$var$setRef(ref, value) { + if (typeof ref === 'function') ref(value);else if (ref !== null && ref !== undefined) ref.current = value; +} +/** + * A utility to compose multiple refs together + * Accepts callback refs and RefObject(s) + */ +function $9c2aaba23466b352$export$43e446d32b3d21af() { + for (var _len = arguments.length, refs = new Array(_len), _key = 0; _key < _len; _key++) { + refs[_key] = arguments[_key]; + } + return node => refs.forEach(ref => $9c2aaba23466b352$var$setRef(ref, node)); +} +/** + * A custom hook that composes multiple refs + * Accepts callback refs and RefObject(s) + */ +function $9c2aaba23466b352$export$c7b2cbe3552a0d05() { + for (var _len2 = arguments.length, refs = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + refs[_key2] = arguments[_key2]; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + return $dJwbH$react.useCallback($9c2aaba23466b352$export$43e446d32b3d21af(...refs), refs); +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-context/dist/index.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-context/dist/index.js ***! + \*******************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $4O1Ne$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "createContext", () => $dec3cc0142d4f286$export$fd42f52fd3ae1109); +$parcel$export(module.exports, "createContextScope", () => $dec3cc0142d4f286$export$50c7b4e9d9f19c1); +function $dec3cc0142d4f286$export$fd42f52fd3ae1109(rootComponentName, defaultContext) { + const Context = /*#__PURE__*/$4O1Ne$react.createContext(defaultContext); + function Provider(props) { + const { + children: children, + ...context + } = props; // Only re-memoize when prop values change + // eslint-disable-next-line react-hooks/exhaustive-deps + const value = $4O1Ne$react.useMemo(() => context, Object.values(context)); + return /*#__PURE__*/$4O1Ne$react.createElement(Context.Provider, { + value: value + }, children); + } + function useContext(consumerName) { + const context = $4O1Ne$react.useContext(Context); + if (context) return context; + if (defaultContext !== undefined) return defaultContext; // if a defaultContext wasn't specified, it's a required context. + throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``); + } + Provider.displayName = rootComponentName + 'Provider'; + return [Provider, useContext]; +} +/* ------------------------------------------------------------------------------------------------- + * createContextScope + * -----------------------------------------------------------------------------------------------*/ +function $dec3cc0142d4f286$export$50c7b4e9d9f19c1(scopeName) { + let createContextScopeDeps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + let defaultContexts = []; + /* ----------------------------------------------------------------------------------------------- + * createContext + * ---------------------------------------------------------------------------------------------*/ + function $dec3cc0142d4f286$export$fd42f52fd3ae1109(rootComponentName, defaultContext) { + const BaseContext = /*#__PURE__*/$4O1Ne$react.createContext(defaultContext); + const index = defaultContexts.length; + defaultContexts = [...defaultContexts, defaultContext]; + function Provider(props) { + const { + scope: scope, + children: children, + ...context + } = props; + const Context = (scope === null || scope === void 0 ? void 0 : scope[scopeName][index]) || BaseContext; // Only re-memoize when prop values change + // eslint-disable-next-line react-hooks/exhaustive-deps + const value = $4O1Ne$react.useMemo(() => context, Object.values(context)); + return /*#__PURE__*/$4O1Ne$react.createElement(Context.Provider, { + value: value + }, children); + } + function useContext(consumerName, scope) { + const Context = (scope === null || scope === void 0 ? void 0 : scope[scopeName][index]) || BaseContext; + const context = $4O1Ne$react.useContext(Context); + if (context) return context; + if (defaultContext !== undefined) return defaultContext; // if a defaultContext wasn't specified, it's a required context. + throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``); + } + Provider.displayName = rootComponentName + 'Provider'; + return [Provider, useContext]; + } + /* ----------------------------------------------------------------------------------------------- + * createScope + * ---------------------------------------------------------------------------------------------*/ + const createScope = () => { + const scopeContexts = defaultContexts.map(defaultContext => { + return /*#__PURE__*/$4O1Ne$react.createContext(defaultContext); + }); + return function useScope(scope) { + const contexts = (scope === null || scope === void 0 ? void 0 : scope[scopeName]) || scopeContexts; + return $4O1Ne$react.useMemo(() => ({ + [`__scope${scopeName}`]: { + ...scope, + [scopeName]: contexts + } + }), [scope, contexts]); + }; + }; + createScope.scopeName = scopeName; + return [$dec3cc0142d4f286$export$fd42f52fd3ae1109, $dec3cc0142d4f286$var$composeContextScopes(createScope, ...createContextScopeDeps)]; +} +/* ------------------------------------------------------------------------------------------------- + * composeContextScopes + * -----------------------------------------------------------------------------------------------*/ +function $dec3cc0142d4f286$var$composeContextScopes() { + for (var _len = arguments.length, scopes = new Array(_len), _key = 0; _key < _len; _key++) { + scopes[_key] = arguments[_key]; + } + const baseScope = scopes[0]; + if (scopes.length === 1) return baseScope; + const createScope1 = () => { + const scopeHooks = scopes.map(createScope => ({ + useScope: createScope(), + scopeName: createScope.scopeName + })); + return function useComposedScopes(overrideScopes) { + const nextScopes1 = scopeHooks.reduce((nextScopes, _ref) => { + let { + useScope: useScope, + scopeName: scopeName + } = _ref; + // We are calling a hook inside a callback which React warns against to avoid inconsistent + // renders, however, scoping doesn't have render side effects so we ignore the rule. + // eslint-disable-next-line react-hooks/rules-of-hooks + const scopeProps = useScope(overrideScopes); + const currentScope = scopeProps[`__scope${scopeName}`]; + return { + ...nextScopes, + ...currentScope + }; + }, {}); + return $4O1Ne$react.useMemo(() => ({ + [`__scope${baseScope.scopeName}`]: nextScopes1 + }), [nextScopes1]); + }; + }; + createScope1.scopeName = baseScope.scopeName; + return createScope1; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-dialog/dist/index.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-dialog/dist/index.js ***! + \******************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $aJCrN$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $aJCrN$react = __webpack_require__(/*! react */ "react"); +var $aJCrN$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $aJCrN$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $aJCrN$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $aJCrN$radixuireactid = __webpack_require__(/*! @radix-ui/react-id */ "../../../node_modules/@radix-ui/react-id/dist/index.js"); +var $aJCrN$radixuireactusecontrollablestate = __webpack_require__(/*! @radix-ui/react-use-controllable-state */ "../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js"); +var $aJCrN$radixuireactdismissablelayer = __webpack_require__(/*! @radix-ui/react-dismissable-layer */ "../../../node_modules/@radix-ui/react-dismissable-layer/dist/index.js"); +var $aJCrN$radixuireactfocusscope = __webpack_require__(/*! @radix-ui/react-focus-scope */ "../../../node_modules/@radix-ui/react-focus-scope/dist/index.js"); +var $aJCrN$radixuireactportal = __webpack_require__(/*! @radix-ui/react-portal */ "../../../node_modules/@radix-ui/react-portal/dist/index.js"); +var $aJCrN$radixuireactpresence = __webpack_require__(/*! @radix-ui/react-presence */ "../../../node_modules/@radix-ui/react-presence/dist/index.js"); +var $aJCrN$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $aJCrN$radixuireactfocusguards = __webpack_require__(/*! @radix-ui/react-focus-guards */ "../../../node_modules/@radix-ui/react-focus-guards/dist/index.js"); +var $aJCrN$reactremovescroll = __webpack_require__(/*! react-remove-scroll */ "../../../node_modules/react-remove-scroll/dist/es2015/index.js"); +var $aJCrN$ariahidden = __webpack_require__(/*! aria-hidden */ "../../../node_modules/aria-hidden/dist/es2015/index.js"); +var $aJCrN$radixuireactslot = __webpack_require__(/*! @radix-ui/react-slot */ "../../../node_modules/@radix-ui/react-slot/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createDialogScope", () => $f4833395aa1bca1a$export$cc702773b8ea3e41); +$parcel$export(module.exports, "Dialog", () => $f4833395aa1bca1a$export$3ddf2d174ce01153); +$parcel$export(module.exports, "DialogTrigger", () => $f4833395aa1bca1a$export$2e1e1122cf0cba88); +$parcel$export(module.exports, "DialogPortal", () => $f4833395aa1bca1a$export$dad7c95542bacce0); +$parcel$export(module.exports, "DialogOverlay", () => $f4833395aa1bca1a$export$bd1d06c79be19e17); +$parcel$export(module.exports, "DialogContent", () => $f4833395aa1bca1a$export$b6d9565de1e068cf); +$parcel$export(module.exports, "DialogTitle", () => $f4833395aa1bca1a$export$16f7638e4a34b909); +$parcel$export(module.exports, "DialogDescription", () => $f4833395aa1bca1a$export$94e94c2ec2c954d5); +$parcel$export(module.exports, "DialogClose", () => $f4833395aa1bca1a$export$fba2fb7cd781b7ac); +$parcel$export(module.exports, "Root", () => $f4833395aa1bca1a$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Trigger", () => $f4833395aa1bca1a$export$41fb9f06171c75f4); +$parcel$export(module.exports, "Portal", () => $f4833395aa1bca1a$export$602eac185826482c); +$parcel$export(module.exports, "Overlay", () => $f4833395aa1bca1a$export$c6fdb837b070b4ff); +$parcel$export(module.exports, "Content", () => $f4833395aa1bca1a$export$7c6e2c02157bb7d2); +$parcel$export(module.exports, "Title", () => $f4833395aa1bca1a$export$f99233281efd08a0); +$parcel$export(module.exports, "Description", () => $f4833395aa1bca1a$export$393edc798c47379d); +$parcel$export(module.exports, "Close", () => $f4833395aa1bca1a$export$f39c2d165cd861fe); +$parcel$export(module.exports, "WarningProvider", () => $f4833395aa1bca1a$export$69b62a49393917d6); + +/* ------------------------------------------------------------------------------------------------- + * Dialog + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$DIALOG_NAME = 'Dialog'; +const [$f4833395aa1bca1a$var$createDialogContext, $f4833395aa1bca1a$export$cc702773b8ea3e41] = $aJCrN$radixuireactcontext.createContextScope($f4833395aa1bca1a$var$DIALOG_NAME); +const [$f4833395aa1bca1a$var$DialogProvider, $f4833395aa1bca1a$var$useDialogContext] = $f4833395aa1bca1a$var$createDialogContext($f4833395aa1bca1a$var$DIALOG_NAME); +const $f4833395aa1bca1a$export$3ddf2d174ce01153 = props => { + const { + __scopeDialog: __scopeDialog, + children: children, + open: openProp, + defaultOpen: defaultOpen, + onOpenChange: onOpenChange, + modal = true + } = props; + const triggerRef = $aJCrN$react.useRef(null); + const contentRef = $aJCrN$react.useRef(null); + const [open = false, setOpen] = $aJCrN$radixuireactusecontrollablestate.useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChange + }); + return /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogProvider, { + scope: __scopeDialog, + triggerRef: triggerRef, + contentRef: contentRef, + contentId: $aJCrN$radixuireactid.useId(), + titleId: $aJCrN$radixuireactid.useId(), + descriptionId: $aJCrN$radixuireactid.useId(), + open: open, + onOpenChange: setOpen, + onOpenToggle: $aJCrN$react.useCallback(() => setOpen(prevOpen => !prevOpen), [setOpen]), + modal: modal + }, children); +}; +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$3ddf2d174ce01153, { + displayName: $f4833395aa1bca1a$var$DIALOG_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DialogTrigger + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$TRIGGER_NAME = 'DialogTrigger'; +const $f4833395aa1bca1a$export$2e1e1122cf0cba88 = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + ...triggerProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$TRIGGER_NAME, __scopeDialog); + const composedTriggerRef = $aJCrN$radixuireactcomposerefs.useComposedRefs(forwardedRef, context.triggerRef); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactprimitive.Primitive.button, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + type: "button", + "aria-haspopup": "dialog", + "aria-expanded": context.open, + "aria-controls": context.contentId, + "data-state": $f4833395aa1bca1a$var$getState(context.open) + }, triggerProps, { + ref: composedTriggerRef, + onClick: $aJCrN$radixuiprimitive.composeEventHandlers(props.onClick, context.onOpenToggle) + })); +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$2e1e1122cf0cba88, { + displayName: $f4833395aa1bca1a$var$TRIGGER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DialogPortal + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$PORTAL_NAME = 'DialogPortal'; +const [$f4833395aa1bca1a$var$PortalProvider, $f4833395aa1bca1a$var$usePortalContext] = $f4833395aa1bca1a$var$createDialogContext($f4833395aa1bca1a$var$PORTAL_NAME, { + forceMount: undefined +}); +const $f4833395aa1bca1a$export$dad7c95542bacce0 = props => { + const { + __scopeDialog: __scopeDialog, + forceMount: forceMount, + children: children, + container: container + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$PORTAL_NAME, __scopeDialog); + return /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$PortalProvider, { + scope: __scopeDialog, + forceMount: forceMount + }, $aJCrN$react.Children.map(children, child => /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactportal.Portal, { + asChild: true, + container: container + }, child)))); +}; +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$dad7c95542bacce0, { + displayName: $f4833395aa1bca1a$var$PORTAL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DialogOverlay + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$OVERLAY_NAME = 'DialogOverlay'; +const $f4833395aa1bca1a$export$bd1d06c79be19e17 = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const portalContext = $f4833395aa1bca1a$var$usePortalContext($f4833395aa1bca1a$var$OVERLAY_NAME, props.__scopeDialog); + const { + forceMount = portalContext.forceMount, + ...overlayProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$OVERLAY_NAME, props.__scopeDialog); + return context.modal ? /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogOverlayImpl, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({}, overlayProps, { + ref: forwardedRef + }))) : null; +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$bd1d06c79be19e17, { + displayName: $f4833395aa1bca1a$var$OVERLAY_NAME +}); +const $f4833395aa1bca1a$var$DialogOverlayImpl = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + ...overlayProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$OVERLAY_NAME, __scopeDialog); + return /*#__PURE__*/ (// Make sure `Content` is scrollable even when it doesn't live inside `RemoveScroll` + // ie. when `Overlay` and `Content` are siblings + $aJCrN$react.createElement($aJCrN$reactremovescroll.RemoveScroll, { + as: $aJCrN$radixuireactslot.Slot, + allowPinchZoom: true, + shards: [context.contentRef] + }, /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactprimitive.Primitive.div, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + "data-state": $f4833395aa1bca1a$var$getState(context.open) + }, overlayProps, { + ref: forwardedRef // We re-enable pointer-events prevented by `Dialog.Content` to allow scrolling the overlay. + , + + style: { + pointerEvents: 'auto', + ...overlayProps.style + } + }))) + ); +}); +/* ------------------------------------------------------------------------------------------------- + * DialogContent + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$CONTENT_NAME = 'DialogContent'; +const $f4833395aa1bca1a$export$b6d9565de1e068cf = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const portalContext = $f4833395aa1bca1a$var$usePortalContext($f4833395aa1bca1a$var$CONTENT_NAME, props.__scopeDialog); + const { + forceMount = portalContext.forceMount, + ...contentProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$CONTENT_NAME, props.__scopeDialog); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactpresence.Presence, { + present: forceMount || context.open + }, context.modal ? /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogContentModal, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({}, contentProps, { + ref: forwardedRef + })) : /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogContentNonModal, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({}, contentProps, { + ref: forwardedRef + }))); +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$b6d9565de1e068cf, { + displayName: $f4833395aa1bca1a$var$CONTENT_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$DialogContentModal = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$CONTENT_NAME, props.__scopeDialog); + const contentRef = $aJCrN$react.useRef(null); + const composedRefs = $aJCrN$radixuireactcomposerefs.useComposedRefs(forwardedRef, context.contentRef, contentRef); // aria-hide everything except the content (better supported equivalent to setting aria-modal) + $aJCrN$react.useEffect(() => { + const content = contentRef.current; + if (content) return $aJCrN$ariahidden.hideOthers(content); + }, []); + return /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogContentImpl, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({}, props, { + ref: composedRefs // we make sure focus isn't trapped once `DialogContent` has been closed + , + + trapFocus: context.open, + disableOutsidePointerEvents: true, + onCloseAutoFocus: $aJCrN$radixuiprimitive.composeEventHandlers(props.onCloseAutoFocus, event => { + var _context$triggerRef$c; + event.preventDefault(); + (_context$triggerRef$c = context.triggerRef.current) === null || _context$triggerRef$c === void 0 || _context$triggerRef$c.focus(); + }), + onPointerDownOutside: $aJCrN$radixuiprimitive.composeEventHandlers(props.onPointerDownOutside, event => { + const originalEvent = event.detail.originalEvent; + const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true; + const isRightClick = originalEvent.button === 2 || ctrlLeftClick; // If the event is a right-click, we shouldn't close because + // it is effectively as if we right-clicked the `Overlay`. + if (isRightClick) event.preventDefault(); + }) // When focus is trapped, a `focusout` event may still happen. + , + + onFocusOutside: $aJCrN$radixuiprimitive.composeEventHandlers(props.onFocusOutside, event => event.preventDefault()) + })); +}); +/* -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$DialogContentNonModal = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$CONTENT_NAME, props.__scopeDialog); + const hasInteractedOutsideRef = $aJCrN$react.useRef(false); + const hasPointerDownOutsideRef = $aJCrN$react.useRef(false); + return /*#__PURE__*/$aJCrN$react.createElement($f4833395aa1bca1a$var$DialogContentImpl, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({}, props, { + ref: forwardedRef, + trapFocus: false, + disableOutsidePointerEvents: false, + onCloseAutoFocus: event => { + var _props$onCloseAutoFoc; + (_props$onCloseAutoFoc = props.onCloseAutoFocus) === null || _props$onCloseAutoFoc === void 0 || _props$onCloseAutoFoc.call(props, event); + if (!event.defaultPrevented) { + var _context$triggerRef$c2; + if (!hasInteractedOutsideRef.current) (_context$triggerRef$c2 = context.triggerRef.current) === null || _context$triggerRef$c2 === void 0 || _context$triggerRef$c2.focus(); // Always prevent auto focus because we either focus manually or want user agent focus + event.preventDefault(); + } + hasInteractedOutsideRef.current = false; + hasPointerDownOutsideRef.current = false; + }, + onInteractOutside: event => { + var _props$onInteractOuts, _context$triggerRef$c3; + (_props$onInteractOuts = props.onInteractOutside) === null || _props$onInteractOuts === void 0 || _props$onInteractOuts.call(props, event); + if (!event.defaultPrevented) { + hasInteractedOutsideRef.current = true; + if (event.detail.originalEvent.type === 'pointerdown') hasPointerDownOutsideRef.current = true; + } // Prevent dismissing when clicking the trigger. + // As the trigger is already setup to close, without doing so would + // cause it to close and immediately open. + const target = event.target; + const targetIsTrigger = (_context$triggerRef$c3 = context.triggerRef.current) === null || _context$triggerRef$c3 === void 0 ? void 0 : _context$triggerRef$c3.contains(target); + if (targetIsTrigger) event.preventDefault(); // On Safari if the trigger is inside a container with tabIndex={0}, when clicked + // we will get the pointer down outside event on the trigger, but then a subsequent + // focus outside event on the container, we ignore any focus outside event when we've + // already had a pointer down outside event. + if (event.detail.originalEvent.type === 'focusin' && hasPointerDownOutsideRef.current) event.preventDefault(); + } + })); +}); +/* -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$DialogContentImpl = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + trapFocus: trapFocus, + onOpenAutoFocus: onOpenAutoFocus, + onCloseAutoFocus: onCloseAutoFocus, + ...contentProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$CONTENT_NAME, __scopeDialog); + const contentRef = $aJCrN$react.useRef(null); + const composedRefs = $aJCrN$radixuireactcomposerefs.useComposedRefs(forwardedRef, contentRef); // Make sure the whole tree has focus guards as our `Dialog` will be + // the last element in the DOM (beacuse of the `Portal`) + $aJCrN$radixuireactfocusguards.useFocusGuards(); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$react.Fragment, null, /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactfocusscope.FocusScope, { + asChild: true, + loop: true, + trapped: trapFocus, + onMountAutoFocus: onOpenAutoFocus, + onUnmountAutoFocus: onCloseAutoFocus + }, /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactdismissablelayer.DismissableLayer, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + role: "dialog", + id: context.contentId, + "aria-describedby": context.descriptionId, + "aria-labelledby": context.titleId, + "data-state": $f4833395aa1bca1a$var$getState(context.open) + }, contentProps, { + ref: composedRefs, + onDismiss: () => context.onOpenChange(false) + }))), false); +}); +/* ------------------------------------------------------------------------------------------------- + * DialogTitle + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$TITLE_NAME = 'DialogTitle'; +const $f4833395aa1bca1a$export$16f7638e4a34b909 = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + ...titleProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$TITLE_NAME, __scopeDialog); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactprimitive.Primitive.h2, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + id: context.titleId + }, titleProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$16f7638e4a34b909, { + displayName: $f4833395aa1bca1a$var$TITLE_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DialogDescription + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$DESCRIPTION_NAME = 'DialogDescription'; +const $f4833395aa1bca1a$export$94e94c2ec2c954d5 = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + ...descriptionProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$DESCRIPTION_NAME, __scopeDialog); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactprimitive.Primitive.p, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + id: context.descriptionId + }, descriptionProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$94e94c2ec2c954d5, { + displayName: $f4833395aa1bca1a$var$DESCRIPTION_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DialogClose + * -----------------------------------------------------------------------------------------------*/ +const $f4833395aa1bca1a$var$CLOSE_NAME = 'DialogClose'; +const $f4833395aa1bca1a$export$fba2fb7cd781b7ac = /*#__PURE__*/$aJCrN$react.forwardRef((props, forwardedRef) => { + const { + __scopeDialog: __scopeDialog, + ...closeProps + } = props; + const context = $f4833395aa1bca1a$var$useDialogContext($f4833395aa1bca1a$var$CLOSE_NAME, __scopeDialog); + return /*#__PURE__*/$aJCrN$react.createElement($aJCrN$radixuireactprimitive.Primitive.button, $parcel$interopDefault($aJCrN$babelruntimehelpersextends)({ + type: "button" + }, closeProps, { + ref: forwardedRef, + onClick: $aJCrN$radixuiprimitive.composeEventHandlers(props.onClick, () => context.onOpenChange(false)) + })); +}); +/*#__PURE__*/ +Object.assign($f4833395aa1bca1a$export$fba2fb7cd781b7ac, { + displayName: $f4833395aa1bca1a$var$CLOSE_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +function $f4833395aa1bca1a$var$getState(open) { + return open ? 'open' : 'closed'; +} +const $f4833395aa1bca1a$var$TITLE_WARNING_NAME = 'DialogTitleWarning'; +const [$f4833395aa1bca1a$export$69b62a49393917d6, $f4833395aa1bca1a$var$useWarningContext] = $aJCrN$radixuireactcontext.createContext($f4833395aa1bca1a$var$TITLE_WARNING_NAME, { + contentName: $f4833395aa1bca1a$var$CONTENT_NAME, + titleName: $f4833395aa1bca1a$var$TITLE_NAME, + docsSlug: 'dialog' +}); +const $f4833395aa1bca1a$var$TitleWarning = _ref => { + let { + titleId: titleId + } = _ref; + const titleWarningContext = $f4833395aa1bca1a$var$useWarningContext($f4833395aa1bca1a$var$TITLE_WARNING_NAME); + const MESSAGE = `\`${titleWarningContext.contentName}\` requires a \`${titleWarningContext.titleName}\` for the component to be accessible for screen reader users. + +If you want to hide the \`${titleWarningContext.titleName}\`, you can wrap it with our VisuallyHidden component. + +For more information, see https://radix-ui.com/primitives/docs/components/${titleWarningContext.docsSlug}`; + $aJCrN$react.useEffect(() => { + if (titleId) { + const hasTitle = document.getElementById(titleId); + if (!hasTitle) throw new Error(MESSAGE); + } + }, [MESSAGE, titleId]); + return null; +}; +const $f4833395aa1bca1a$var$DESCRIPTION_WARNING_NAME = 'DialogDescriptionWarning'; +const $f4833395aa1bca1a$var$DescriptionWarning = _ref2 => { + let { + contentRef: contentRef, + descriptionId: descriptionId + } = _ref2; + const descriptionWarningContext = $f4833395aa1bca1a$var$useWarningContext($f4833395aa1bca1a$var$DESCRIPTION_WARNING_NAME); + const MESSAGE = `Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${descriptionWarningContext.contentName}}.`; + $aJCrN$react.useEffect(() => { + var _contentRef$current; + const describedById = (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 ? void 0 : _contentRef$current.getAttribute('aria-describedby'); // if we have an id and the user hasn't set aria-describedby={undefined} + if (descriptionId && describedById) { + const hasDescription = document.getElementById(descriptionId); + if (!hasDescription) console.warn(MESSAGE); + } + }, [MESSAGE, contentRef, descriptionId]); + return null; +}; +const $f4833395aa1bca1a$export$be92b6f5f03c0fe9 = $f4833395aa1bca1a$export$3ddf2d174ce01153; +const $f4833395aa1bca1a$export$41fb9f06171c75f4 = $f4833395aa1bca1a$export$2e1e1122cf0cba88; +const $f4833395aa1bca1a$export$602eac185826482c = $f4833395aa1bca1a$export$dad7c95542bacce0; +const $f4833395aa1bca1a$export$c6fdb837b070b4ff = $f4833395aa1bca1a$export$bd1d06c79be19e17; +const $f4833395aa1bca1a$export$7c6e2c02157bb7d2 = $f4833395aa1bca1a$export$b6d9565de1e068cf; +const $f4833395aa1bca1a$export$f99233281efd08a0 = $f4833395aa1bca1a$export$16f7638e4a34b909; +const $f4833395aa1bca1a$export$393edc798c47379d = $f4833395aa1bca1a$export$94e94c2ec2c954d5; +const $f4833395aa1bca1a$export$f39c2d165cd861fe = $f4833395aa1bca1a$export$fba2fb7cd781b7ac; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-direction/dist/index.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-direction/dist/index.js ***! + \*********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $9g4ps$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useDirection", () => $cc45c1b701a63adc$export$b39126d51d94e6f3); +$parcel$export(module.exports, "Provider", () => $cc45c1b701a63adc$export$2881499e37b75b9a); +$parcel$export(module.exports, "DirectionProvider", () => $cc45c1b701a63adc$export$c760c09fdd558351); +const $cc45c1b701a63adc$var$DirectionContext = /*#__PURE__*/$9g4ps$react.createContext(undefined); +/* ------------------------------------------------------------------------------------------------- + * Direction + * -----------------------------------------------------------------------------------------------*/ +const $cc45c1b701a63adc$export$c760c09fdd558351 = props => { + const { + dir: dir, + children: children + } = props; + return /*#__PURE__*/$9g4ps$react.createElement($cc45c1b701a63adc$var$DirectionContext.Provider, { + value: dir + }, children); +}; +/* -----------------------------------------------------------------------------------------------*/ +function $cc45c1b701a63adc$export$b39126d51d94e6f3(localDir) { + const globalDir = $9g4ps$react.useContext($cc45c1b701a63adc$var$DirectionContext); + return localDir || globalDir || 'ltr'; +} +const $cc45c1b701a63adc$export$2881499e37b75b9a = $cc45c1b701a63adc$export$c760c09fdd558351; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-dismissable-layer/dist/index.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-dismissable-layer/dist/index.js ***! + \*****************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $g2vWm$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $g2vWm$react = __webpack_require__(/*! react */ "react"); +var $g2vWm$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $g2vWm$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $g2vWm$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $g2vWm$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +var $g2vWm$radixuireactuseescapekeydown = __webpack_require__(/*! @radix-ui/react-use-escape-keydown */ "../../../node_modules/@radix-ui/react-use-escape-keydown/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "DismissableLayer", () => $d715e0554b679f1f$export$177fb62ff3ec1f22); +$parcel$export(module.exports, "DismissableLayerBranch", () => $d715e0554b679f1f$export$4d5eb2109db14228); +$parcel$export(module.exports, "Root", () => $d715e0554b679f1f$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Branch", () => $d715e0554b679f1f$export$aecb2ddcb55c95be); + +/* ------------------------------------------------------------------------------------------------- + * DismissableLayer + * -----------------------------------------------------------------------------------------------*/ +const $d715e0554b679f1f$var$DISMISSABLE_LAYER_NAME = 'DismissableLayer'; +const $d715e0554b679f1f$var$CONTEXT_UPDATE = 'dismissableLayer.update'; +const $d715e0554b679f1f$var$POINTER_DOWN_OUTSIDE = 'dismissableLayer.pointerDownOutside'; +const $d715e0554b679f1f$var$FOCUS_OUTSIDE = 'dismissableLayer.focusOutside'; +let $d715e0554b679f1f$var$originalBodyPointerEvents; +const $d715e0554b679f1f$var$DismissableLayerContext = /*#__PURE__*/$g2vWm$react.createContext({ + layers: new Set(), + layersWithOutsidePointerEventsDisabled: new Set(), + branches: new Set() +}); +const $d715e0554b679f1f$export$177fb62ff3ec1f22 = /*#__PURE__*/$g2vWm$react.forwardRef((props, forwardedRef) => { + var _node$ownerDocument; + const { + disableOutsidePointerEvents = false, + onEscapeKeyDown: onEscapeKeyDown, + onPointerDownOutside: onPointerDownOutside, + onFocusOutside: onFocusOutside, + onInteractOutside: onInteractOutside, + onDismiss: onDismiss, + ...layerProps + } = props; + const context = $g2vWm$react.useContext($d715e0554b679f1f$var$DismissableLayerContext); + const [node1, setNode] = $g2vWm$react.useState(null); + const ownerDocument = (_node$ownerDocument = node1 === null || node1 === void 0 ? void 0 : node1.ownerDocument) !== null && _node$ownerDocument !== void 0 ? _node$ownerDocument : globalThis === null || globalThis === void 0 ? void 0 : globalThis.document; + const [, force] = $g2vWm$react.useState({}); + const composedRefs = $g2vWm$radixuireactcomposerefs.useComposedRefs(forwardedRef, node => setNode(node)); + const layers = Array.from(context.layers); + const [highestLayerWithOutsidePointerEventsDisabled] = [...context.layersWithOutsidePointerEventsDisabled].slice(-1); // prettier-ignore + const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(highestLayerWithOutsidePointerEventsDisabled); // prettier-ignore + const index = node1 ? layers.indexOf(node1) : -1; + const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0; + const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex; + const pointerDownOutside = $d715e0554b679f1f$var$usePointerDownOutside(event => { + const target = event.target; + const isPointerDownOnBranch = [...context.branches].some(branch => branch.contains(target)); + if (!isPointerEventsEnabled || isPointerDownOnBranch) return; + onPointerDownOutside === null || onPointerDownOutside === void 0 || onPointerDownOutside(event); + onInteractOutside === null || onInteractOutside === void 0 || onInteractOutside(event); + if (!event.defaultPrevented) onDismiss === null || onDismiss === void 0 || onDismiss(); + }, ownerDocument); + const focusOutside = $d715e0554b679f1f$var$useFocusOutside(event => { + const target = event.target; + const isFocusInBranch = [...context.branches].some(branch => branch.contains(target)); + if (isFocusInBranch) return; + onFocusOutside === null || onFocusOutside === void 0 || onFocusOutside(event); + onInteractOutside === null || onInteractOutside === void 0 || onInteractOutside(event); + if (!event.defaultPrevented) onDismiss === null || onDismiss === void 0 || onDismiss(); + }, ownerDocument); + $g2vWm$radixuireactuseescapekeydown.useEscapeKeydown(event => { + const isHighestLayer = index === context.layers.size - 1; + if (!isHighestLayer) return; + onEscapeKeyDown === null || onEscapeKeyDown === void 0 || onEscapeKeyDown(event); + if (!event.defaultPrevented && onDismiss) { + event.preventDefault(); + onDismiss(); + } + }, ownerDocument); + $g2vWm$react.useEffect(() => { + if (!node1) return; + if (disableOutsidePointerEvents) { + if (context.layersWithOutsidePointerEventsDisabled.size === 0) { + $d715e0554b679f1f$var$originalBodyPointerEvents = ownerDocument.body.style.pointerEvents; + ownerDocument.body.style.pointerEvents = 'none'; + } + context.layersWithOutsidePointerEventsDisabled.add(node1); + } + context.layers.add(node1); + $d715e0554b679f1f$var$dispatchUpdate(); + return () => { + if (disableOutsidePointerEvents && context.layersWithOutsidePointerEventsDisabled.size === 1) ownerDocument.body.style.pointerEvents = $d715e0554b679f1f$var$originalBodyPointerEvents; + }; + }, [node1, ownerDocument, disableOutsidePointerEvents, context]); + /** + * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect + * because a change to `disableOutsidePointerEvents` would remove this layer from the stack + * and add it to the end again so the layering order wouldn't be _creation order_. + * We only want them to be removed from context stacks when unmounted. + */ + $g2vWm$react.useEffect(() => { + return () => { + if (!node1) return; + context.layers.delete(node1); + context.layersWithOutsidePointerEventsDisabled.delete(node1); + $d715e0554b679f1f$var$dispatchUpdate(); + }; + }, [node1, context]); + $g2vWm$react.useEffect(() => { + const handleUpdate = () => force({}); + document.addEventListener($d715e0554b679f1f$var$CONTEXT_UPDATE, handleUpdate); + return () => document.removeEventListener($d715e0554b679f1f$var$CONTEXT_UPDATE, handleUpdate); + }, []); + return /*#__PURE__*/$g2vWm$react.createElement($g2vWm$radixuireactprimitive.Primitive.div, $parcel$interopDefault($g2vWm$babelruntimehelpersextends)({}, layerProps, { + ref: composedRefs, + style: { + pointerEvents: isBodyPointerEventsDisabled ? isPointerEventsEnabled ? 'auto' : 'none' : undefined, + ...props.style + }, + onFocusCapture: $g2vWm$radixuiprimitive.composeEventHandlers(props.onFocusCapture, focusOutside.onFocusCapture), + onBlurCapture: $g2vWm$radixuiprimitive.composeEventHandlers(props.onBlurCapture, focusOutside.onBlurCapture), + onPointerDownCapture: $g2vWm$radixuiprimitive.composeEventHandlers(props.onPointerDownCapture, pointerDownOutside.onPointerDownCapture) + })); +}); +/*#__PURE__*/ +Object.assign($d715e0554b679f1f$export$177fb62ff3ec1f22, { + displayName: $d715e0554b679f1f$var$DISMISSABLE_LAYER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DismissableLayerBranch + * -----------------------------------------------------------------------------------------------*/ +const $d715e0554b679f1f$var$BRANCH_NAME = 'DismissableLayerBranch'; +const $d715e0554b679f1f$export$4d5eb2109db14228 = /*#__PURE__*/$g2vWm$react.forwardRef((props, forwardedRef) => { + const context = $g2vWm$react.useContext($d715e0554b679f1f$var$DismissableLayerContext); + const ref = $g2vWm$react.useRef(null); + const composedRefs = $g2vWm$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + $g2vWm$react.useEffect(() => { + const node = ref.current; + if (node) { + context.branches.add(node); + return () => { + context.branches.delete(node); + }; + } + }, [context.branches]); + return /*#__PURE__*/$g2vWm$react.createElement($g2vWm$radixuireactprimitive.Primitive.div, $parcel$interopDefault($g2vWm$babelruntimehelpersextends)({}, props, { + ref: composedRefs + })); +}); +/*#__PURE__*/ +Object.assign($d715e0554b679f1f$export$4d5eb2109db14228, { + displayName: $d715e0554b679f1f$var$BRANCH_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ /** + * Listens for `pointerdown` outside a react subtree. We use `pointerdown` rather than `pointerup` + * to mimic layer dismissing behaviour present in OS. + * Returns props to pass to the node we want to check for outside events. + */ +function $d715e0554b679f1f$var$usePointerDownOutside(onPointerDownOutside) { + let ownerDocument = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalThis === null || globalThis === void 0 ? void 0 : globalThis.document; + const handlePointerDownOutside = $g2vWm$radixuireactusecallbackref.useCallbackRef(onPointerDownOutside); + const isPointerInsideReactTreeRef = $g2vWm$react.useRef(false); + const handleClickRef = $g2vWm$react.useRef(() => {}); + $g2vWm$react.useEffect(() => { + const handlePointerDown = event => { + if (event.target && !isPointerInsideReactTreeRef.current) { + const eventDetail = { + originalEvent: event + }; + function handleAndDispatchPointerDownOutsideEvent() { + $d715e0554b679f1f$var$handleAndDispatchCustomEvent($d715e0554b679f1f$var$POINTER_DOWN_OUTSIDE, handlePointerDownOutside, eventDetail, { + discrete: true + }); + } + /** + * On touch devices, we need to wait for a click event because browsers implement + * a ~350ms delay between the time the user stops touching the display and when the + * browser executres events. We need to ensure we don't reactivate pointer-events within + * this timeframe otherwise the browser may execute events that should have been prevented. + * + * Additionally, this also lets us deal automatically with cancellations when a click event + * isn't raised because the page was considered scrolled/drag-scrolled, long-pressed, etc. + * + * This is why we also continuously remove the previous listener, because we cannot be + * certain that it was raised, and therefore cleaned-up. + */ + if (event.pointerType === 'touch') { + ownerDocument.removeEventListener('click', handleClickRef.current); + handleClickRef.current = handleAndDispatchPointerDownOutsideEvent; + ownerDocument.addEventListener('click', handleClickRef.current, { + once: true + }); + } else handleAndDispatchPointerDownOutsideEvent(); + } + isPointerInsideReactTreeRef.current = false; + }; + /** + * if this hook executes in a component that mounts via a `pointerdown` event, the event + * would bubble up to the document and trigger a `pointerDownOutside` event. We avoid + * this by delaying the event listener registration on the document. + * This is not React specific, but rather how the DOM works, ie: + * ``` + * button.addEventListener('pointerdown', () => { + * console.log('I will log'); + * document.addEventListener('pointerdown', () => { + * console.log('I will also log'); + * }) + * }); + */ + const timerId = window.setTimeout(() => { + ownerDocument.addEventListener('pointerdown', handlePointerDown); + }, 0); + return () => { + window.clearTimeout(timerId); + ownerDocument.removeEventListener('pointerdown', handlePointerDown); + ownerDocument.removeEventListener('click', handleClickRef.current); + }; + }, [ownerDocument, handlePointerDownOutside]); + return { + // ensures we check React component tree (not just DOM tree) + onPointerDownCapture: () => isPointerInsideReactTreeRef.current = true + }; +} +/** + * Listens for when focus happens outside a react subtree. + * Returns props to pass to the root (node) of the subtree we want to check. + */ +function $d715e0554b679f1f$var$useFocusOutside(onFocusOutside) { + let ownerDocument = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalThis === null || globalThis === void 0 ? void 0 : globalThis.document; + const handleFocusOutside = $g2vWm$radixuireactusecallbackref.useCallbackRef(onFocusOutside); + const isFocusInsideReactTreeRef = $g2vWm$react.useRef(false); + $g2vWm$react.useEffect(() => { + const handleFocus = event => { + if (event.target && !isFocusInsideReactTreeRef.current) { + const eventDetail = { + originalEvent: event + }; + $d715e0554b679f1f$var$handleAndDispatchCustomEvent($d715e0554b679f1f$var$FOCUS_OUTSIDE, handleFocusOutside, eventDetail, { + discrete: false + }); + } + }; + ownerDocument.addEventListener('focusin', handleFocus); + return () => ownerDocument.removeEventListener('focusin', handleFocus); + }, [ownerDocument, handleFocusOutside]); + return { + onFocusCapture: () => isFocusInsideReactTreeRef.current = true, + onBlurCapture: () => isFocusInsideReactTreeRef.current = false + }; +} +function $d715e0554b679f1f$var$dispatchUpdate() { + const event = new CustomEvent($d715e0554b679f1f$var$CONTEXT_UPDATE); + document.dispatchEvent(event); +} +function $d715e0554b679f1f$var$handleAndDispatchCustomEvent(name, handler, detail, _ref) { + let { + discrete: discrete + } = _ref; + const target = detail.originalEvent.target; + const event = new CustomEvent(name, { + bubbles: false, + cancelable: true, + detail: detail + }); + if (handler) target.addEventListener(name, handler, { + once: true + }); + if (discrete) $g2vWm$radixuireactprimitive.dispatchDiscreteCustomEvent(target, event);else target.dispatchEvent(event); +} +const $d715e0554b679f1f$export$be92b6f5f03c0fe9 = $d715e0554b679f1f$export$177fb62ff3ec1f22; +const $d715e0554b679f1f$export$aecb2ddcb55c95be = $d715e0554b679f1f$export$4d5eb2109db14228; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-dropdown-menu/dist/index.js": +/*!*************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-dropdown-menu/dist/index.js ***! + \*************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $7dQ7Q$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $7dQ7Q$react = __webpack_require__(/*! react */ "react"); +var $7dQ7Q$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $7dQ7Q$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $7dQ7Q$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $7dQ7Q$radixuireactusecontrollablestate = __webpack_require__(/*! @radix-ui/react-use-controllable-state */ "../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js"); +var $7dQ7Q$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $7dQ7Q$radixuireactmenu = __webpack_require__(/*! @radix-ui/react-menu */ "../../../node_modules/@radix-ui/react-menu/dist/index.js"); +var $7dQ7Q$radixuireactid = __webpack_require__(/*! @radix-ui/react-id */ "../../../node_modules/@radix-ui/react-id/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createDropdownMenuScope", () => $d1bf075a6b218014$export$c0623cd925aeb687); +$parcel$export(module.exports, "DropdownMenu", () => $d1bf075a6b218014$export$e44a253a59704894); +$parcel$export(module.exports, "DropdownMenuTrigger", () => $d1bf075a6b218014$export$d2469213b3befba9); +$parcel$export(module.exports, "DropdownMenuPortal", () => $d1bf075a6b218014$export$cd369b4d4d54efc9); +$parcel$export(module.exports, "DropdownMenuContent", () => $d1bf075a6b218014$export$6e76d93a37c01248); +$parcel$export(module.exports, "DropdownMenuGroup", () => $d1bf075a6b218014$export$246bebaba3a2f70e); +$parcel$export(module.exports, "DropdownMenuLabel", () => $d1bf075a6b218014$export$76e48c5b57f24495); +$parcel$export(module.exports, "DropdownMenuItem", () => $d1bf075a6b218014$export$ed97964d1871885d); +$parcel$export(module.exports, "DropdownMenuCheckboxItem", () => $d1bf075a6b218014$export$53a69729da201fa9); +$parcel$export(module.exports, "DropdownMenuRadioGroup", () => $d1bf075a6b218014$export$3323ad73d55f587e); +$parcel$export(module.exports, "DropdownMenuRadioItem", () => $d1bf075a6b218014$export$e4f69b41b1637536); +$parcel$export(module.exports, "DropdownMenuItemIndicator", () => $d1bf075a6b218014$export$42355ae145153fb6); +$parcel$export(module.exports, "DropdownMenuSeparator", () => $d1bf075a6b218014$export$da160178fd3bc7e9); +$parcel$export(module.exports, "DropdownMenuArrow", () => $d1bf075a6b218014$export$34b8980744021ec5); +$parcel$export(module.exports, "DropdownMenuSub", () => $d1bf075a6b218014$export$2f307d81a64f5442); +$parcel$export(module.exports, "DropdownMenuSubTrigger", () => $d1bf075a6b218014$export$21dcb7ec56f874cf); +$parcel$export(module.exports, "DropdownMenuSubContent", () => $d1bf075a6b218014$export$f34ec8bc2482cc5f); +$parcel$export(module.exports, "Root", () => $d1bf075a6b218014$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Trigger", () => $d1bf075a6b218014$export$41fb9f06171c75f4); +$parcel$export(module.exports, "Portal", () => $d1bf075a6b218014$export$602eac185826482c); +$parcel$export(module.exports, "Content", () => $d1bf075a6b218014$export$7c6e2c02157bb7d2); +$parcel$export(module.exports, "Group", () => $d1bf075a6b218014$export$eb2fcfdbd7ba97d4); +$parcel$export(module.exports, "Label", () => $d1bf075a6b218014$export$b04be29aa201d4f5); +$parcel$export(module.exports, "Item", () => $d1bf075a6b218014$export$6d08773d2e66f8f2); +$parcel$export(module.exports, "CheckboxItem", () => $d1bf075a6b218014$export$16ce288f89fa631c); +$parcel$export(module.exports, "RadioGroup", () => $d1bf075a6b218014$export$a98f0dcb43a68a25); +$parcel$export(module.exports, "RadioItem", () => $d1bf075a6b218014$export$371ab307eab489c0); +$parcel$export(module.exports, "ItemIndicator", () => $d1bf075a6b218014$export$c3468e2714d175fa); +$parcel$export(module.exports, "Separator", () => $d1bf075a6b218014$export$1ff3c3f08ae963c0); +$parcel$export(module.exports, "Arrow", () => $d1bf075a6b218014$export$21b07c8f274aebd5); +$parcel$export(module.exports, "Sub", () => $d1bf075a6b218014$export$d7a01e11500dfb6f); +$parcel$export(module.exports, "SubTrigger", () => $d1bf075a6b218014$export$2ea8a7a591ac5eac); +$parcel$export(module.exports, "SubContent", () => $d1bf075a6b218014$export$6d4de93b380beddf); + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenu + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$DROPDOWN_MENU_NAME = 'DropdownMenu'; +const [$d1bf075a6b218014$var$createDropdownMenuContext, $d1bf075a6b218014$export$c0623cd925aeb687] = $7dQ7Q$radixuireactcontext.createContextScope($d1bf075a6b218014$var$DROPDOWN_MENU_NAME, [$7dQ7Q$radixuireactmenu.createMenuScope]); +const $d1bf075a6b218014$var$useMenuScope = $7dQ7Q$radixuireactmenu.createMenuScope(); +const [$d1bf075a6b218014$var$DropdownMenuProvider, $d1bf075a6b218014$var$useDropdownMenuContext] = $d1bf075a6b218014$var$createDropdownMenuContext($d1bf075a6b218014$var$DROPDOWN_MENU_NAME); +const $d1bf075a6b218014$export$e44a253a59704894 = props => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + children: children, + dir: dir, + open: openProp, + defaultOpen: defaultOpen, + onOpenChange: onOpenChange, + modal = true + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + const triggerRef = $7dQ7Q$react.useRef(null); + const [open = false, setOpen] = $7dQ7Q$radixuireactusecontrollablestate.useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChange + }); + return /*#__PURE__*/$7dQ7Q$react.createElement($d1bf075a6b218014$var$DropdownMenuProvider, { + scope: __scopeDropdownMenu, + triggerId: $7dQ7Q$radixuireactid.useId(), + triggerRef: triggerRef, + contentId: $7dQ7Q$radixuireactid.useId(), + open: open, + onOpenChange: setOpen, + onOpenToggle: $7dQ7Q$react.useCallback(() => setOpen(prevOpen => !prevOpen), [setOpen]), + modal: modal + }, /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Root, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, { + open: open, + onOpenChange: setOpen, + dir: dir, + modal: modal + }), children)); +}; +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$e44a253a59704894, { + displayName: $d1bf075a6b218014$var$DROPDOWN_MENU_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuTrigger + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$TRIGGER_NAME = 'DropdownMenuTrigger'; +const $d1bf075a6b218014$export$d2469213b3befba9 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + disabled = false, + ...triggerProps + } = props; + const context = $d1bf075a6b218014$var$useDropdownMenuContext($d1bf075a6b218014$var$TRIGGER_NAME, __scopeDropdownMenu); + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Anchor, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({ + asChild: true + }, menuScope), /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactprimitive.Primitive.button, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({ + type: "button", + id: context.triggerId, + "aria-haspopup": "menu", + "aria-expanded": context.open, + "aria-controls": context.open ? context.contentId : undefined, + "data-state": context.open ? 'open' : 'closed', + "data-disabled": disabled ? '' : undefined, + disabled: disabled + }, triggerProps, { + ref: $7dQ7Q$radixuireactcomposerefs.composeRefs(forwardedRef, context.triggerRef), + onPointerDown: $7dQ7Q$radixuiprimitive.composeEventHandlers(props.onPointerDown, event => { + // only call handler if it's the left button (mousedown gets triggered by all mouse buttons) + // but not when the control key is pressed (avoiding MacOS right click) + if (!disabled && event.button === 0 && event.ctrlKey === false) { + context.onOpenToggle(); // prevent trigger focusing when opening + // this allows the content to be given focus without competition + if (!context.open) event.preventDefault(); + } + }), + onKeyDown: $7dQ7Q$radixuiprimitive.composeEventHandlers(props.onKeyDown, event => { + if (disabled) return; + if (['Enter', ' '].includes(event.key)) context.onOpenToggle(); + if (event.key === 'ArrowDown') context.onOpenChange(true); // prevent keydown from scrolling window / first focused item to execute + // that keydown (inadvertently closing the menu) + if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault(); + }) + }))); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$d2469213b3befba9, { + displayName: $d1bf075a6b218014$var$TRIGGER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuPortal + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$PORTAL_NAME = 'DropdownMenuPortal'; +const $d1bf075a6b218014$export$cd369b4d4d54efc9 = props => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...portalProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Portal, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, portalProps)); +}; +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$cd369b4d4d54efc9, { + displayName: $d1bf075a6b218014$var$PORTAL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuContent + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$CONTENT_NAME = 'DropdownMenuContent'; +const $d1bf075a6b218014$export$6e76d93a37c01248 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...contentProps + } = props; + const context = $d1bf075a6b218014$var$useDropdownMenuContext($d1bf075a6b218014$var$CONTENT_NAME, __scopeDropdownMenu); + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + const hasInteractedOutsideRef = $7dQ7Q$react.useRef(false); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Content, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({ + id: context.contentId, + "aria-labelledby": context.triggerId + }, menuScope, contentProps, { + ref: forwardedRef, + onCloseAutoFocus: $7dQ7Q$radixuiprimitive.composeEventHandlers(props.onCloseAutoFocus, event => { + var _context$triggerRef$c; + if (!hasInteractedOutsideRef.current) (_context$triggerRef$c = context.triggerRef.current) === null || _context$triggerRef$c === void 0 || _context$triggerRef$c.focus(); + hasInteractedOutsideRef.current = false; // Always prevent auto focus because we either focus manually or want user agent focus + event.preventDefault(); + }), + onInteractOutside: $7dQ7Q$radixuiprimitive.composeEventHandlers(props.onInteractOutside, event => { + const originalEvent = event.detail.originalEvent; + const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true; + const isRightClick = originalEvent.button === 2 || ctrlLeftClick; + if (!context.modal || isRightClick) hasInteractedOutsideRef.current = true; + }), + style: { + ...props.style, + '--radix-dropdown-menu-content-transform-origin': 'var(--radix-popper-transform-origin)', + '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)', + '--radix-dropdown-menu-content-available-height': 'var(--radix-popper-available-height)', + '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)', + '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)' + } + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$6e76d93a37c01248, { + displayName: $d1bf075a6b218014$var$CONTENT_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuGroup + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$GROUP_NAME = 'DropdownMenuGroup'; +const $d1bf075a6b218014$export$246bebaba3a2f70e = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...groupProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Group, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, groupProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$246bebaba3a2f70e, { + displayName: $d1bf075a6b218014$var$GROUP_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuLabel + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$LABEL_NAME = 'DropdownMenuLabel'; +const $d1bf075a6b218014$export$76e48c5b57f24495 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...labelProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Label, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, labelProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$76e48c5b57f24495, { + displayName: $d1bf075a6b218014$var$LABEL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuItem + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$ITEM_NAME = 'DropdownMenuItem'; +const $d1bf075a6b218014$export$ed97964d1871885d = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...itemProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Item, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, itemProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$ed97964d1871885d, { + displayName: $d1bf075a6b218014$var$ITEM_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuCheckboxItem + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$CHECKBOX_ITEM_NAME = 'DropdownMenuCheckboxItem'; +const $d1bf075a6b218014$export$53a69729da201fa9 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...checkboxItemProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.CheckboxItem, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, checkboxItemProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$53a69729da201fa9, { + displayName: $d1bf075a6b218014$var$CHECKBOX_ITEM_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuRadioGroup + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$RADIO_GROUP_NAME = 'DropdownMenuRadioGroup'; +const $d1bf075a6b218014$export$3323ad73d55f587e = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...radioGroupProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.RadioGroup, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, radioGroupProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$3323ad73d55f587e, { + displayName: $d1bf075a6b218014$var$RADIO_GROUP_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuRadioItem + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$RADIO_ITEM_NAME = 'DropdownMenuRadioItem'; +const $d1bf075a6b218014$export$e4f69b41b1637536 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...radioItemProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.RadioItem, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, radioItemProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$e4f69b41b1637536, { + displayName: $d1bf075a6b218014$var$RADIO_ITEM_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuItemIndicator + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$INDICATOR_NAME = 'DropdownMenuItemIndicator'; +const $d1bf075a6b218014$export$42355ae145153fb6 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...itemIndicatorProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.ItemIndicator, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, itemIndicatorProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$42355ae145153fb6, { + displayName: $d1bf075a6b218014$var$INDICATOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSeparator + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$SEPARATOR_NAME = 'DropdownMenuSeparator'; +const $d1bf075a6b218014$export$da160178fd3bc7e9 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...separatorProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Separator, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, separatorProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$da160178fd3bc7e9, { + displayName: $d1bf075a6b218014$var$SEPARATOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuArrow + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$ARROW_NAME = 'DropdownMenuArrow'; +const $d1bf075a6b218014$export$34b8980744021ec5 = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...arrowProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Arrow, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, arrowProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$34b8980744021ec5, { + displayName: $d1bf075a6b218014$var$ARROW_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSub + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$export$2f307d81a64f5442 = props => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + children: children, + open: openProp, + onOpenChange: onOpenChange, + defaultOpen: defaultOpen + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + const [open = false, setOpen] = $7dQ7Q$radixuireactusecontrollablestate.useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChange + }); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.Sub, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, { + open: open, + onOpenChange: setOpen + }), children); +}; +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSubTrigger + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$SUB_TRIGGER_NAME = 'DropdownMenuSubTrigger'; +const $d1bf075a6b218014$export$21dcb7ec56f874cf = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...subTriggerProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.SubTrigger, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, subTriggerProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$21dcb7ec56f874cf, { + displayName: $d1bf075a6b218014$var$SUB_TRIGGER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSubContent + * -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$var$SUB_CONTENT_NAME = 'DropdownMenuSubContent'; +const $d1bf075a6b218014$export$f34ec8bc2482cc5f = /*#__PURE__*/$7dQ7Q$react.forwardRef((props, forwardedRef) => { + const { + __scopeDropdownMenu: __scopeDropdownMenu, + ...subContentProps + } = props; + const menuScope = $d1bf075a6b218014$var$useMenuScope(__scopeDropdownMenu); + return /*#__PURE__*/$7dQ7Q$react.createElement($7dQ7Q$radixuireactmenu.SubContent, $parcel$interopDefault($7dQ7Q$babelruntimehelpersextends)({}, menuScope, subContentProps, { + ref: forwardedRef, + style: { + ...props.style, + '--radix-dropdown-menu-content-transform-origin': 'var(--radix-popper-transform-origin)', + '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)', + '--radix-dropdown-menu-content-available-height': 'var(--radix-popper-available-height)', + '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)', + '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)' + } + })); +}); +/*#__PURE__*/ +Object.assign($d1bf075a6b218014$export$f34ec8bc2482cc5f, { + displayName: $d1bf075a6b218014$var$SUB_CONTENT_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $d1bf075a6b218014$export$be92b6f5f03c0fe9 = $d1bf075a6b218014$export$e44a253a59704894; +const $d1bf075a6b218014$export$41fb9f06171c75f4 = $d1bf075a6b218014$export$d2469213b3befba9; +const $d1bf075a6b218014$export$602eac185826482c = $d1bf075a6b218014$export$cd369b4d4d54efc9; +const $d1bf075a6b218014$export$7c6e2c02157bb7d2 = $d1bf075a6b218014$export$6e76d93a37c01248; +const $d1bf075a6b218014$export$eb2fcfdbd7ba97d4 = $d1bf075a6b218014$export$246bebaba3a2f70e; +const $d1bf075a6b218014$export$b04be29aa201d4f5 = $d1bf075a6b218014$export$76e48c5b57f24495; +const $d1bf075a6b218014$export$6d08773d2e66f8f2 = $d1bf075a6b218014$export$ed97964d1871885d; +const $d1bf075a6b218014$export$16ce288f89fa631c = $d1bf075a6b218014$export$53a69729da201fa9; +const $d1bf075a6b218014$export$a98f0dcb43a68a25 = $d1bf075a6b218014$export$3323ad73d55f587e; +const $d1bf075a6b218014$export$371ab307eab489c0 = $d1bf075a6b218014$export$e4f69b41b1637536; +const $d1bf075a6b218014$export$c3468e2714d175fa = $d1bf075a6b218014$export$42355ae145153fb6; +const $d1bf075a6b218014$export$1ff3c3f08ae963c0 = $d1bf075a6b218014$export$da160178fd3bc7e9; +const $d1bf075a6b218014$export$21b07c8f274aebd5 = $d1bf075a6b218014$export$34b8980744021ec5; +const $d1bf075a6b218014$export$d7a01e11500dfb6f = $d1bf075a6b218014$export$2f307d81a64f5442; +const $d1bf075a6b218014$export$2ea8a7a591ac5eac = $d1bf075a6b218014$export$21dcb7ec56f874cf; +const $d1bf075a6b218014$export$6d4de93b380beddf = $d1bf075a6b218014$export$f34ec8bc2482cc5f; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-focus-guards/dist/index.js": +/*!************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-focus-guards/dist/index.js ***! + \************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $cnctE$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "FocusGuards", () => $71476a6ed7dbbaf3$export$ac5b58043b79449b); +$parcel$export(module.exports, "Root", () => $71476a6ed7dbbaf3$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "useFocusGuards", () => $71476a6ed7dbbaf3$export$b7ece24a22aeda8c); + +/** Number of components which have requested interest to have focus guards */ +let $71476a6ed7dbbaf3$var$count = 0; +function $71476a6ed7dbbaf3$export$ac5b58043b79449b(props) { + $71476a6ed7dbbaf3$export$b7ece24a22aeda8c(); + return props.children; +} +/** + * Injects a pair of focus guards at the edges of the whole DOM tree + * to ensure `focusin` & `focusout` events can be caught consistently. + */ +function $71476a6ed7dbbaf3$export$b7ece24a22aeda8c() { + $cnctE$react.useEffect(() => { + var _edgeGuards$, _edgeGuards$2; + const edgeGuards = document.querySelectorAll('[data-radix-focus-guard]'); + document.body.insertAdjacentElement('afterbegin', (_edgeGuards$ = edgeGuards[0]) !== null && _edgeGuards$ !== void 0 ? _edgeGuards$ : $71476a6ed7dbbaf3$var$createFocusGuard()); + document.body.insertAdjacentElement('beforeend', (_edgeGuards$2 = edgeGuards[1]) !== null && _edgeGuards$2 !== void 0 ? _edgeGuards$2 : $71476a6ed7dbbaf3$var$createFocusGuard()); + $71476a6ed7dbbaf3$var$count++; + return () => { + if ($71476a6ed7dbbaf3$var$count === 1) document.querySelectorAll('[data-radix-focus-guard]').forEach(node => node.remove()); + $71476a6ed7dbbaf3$var$count--; + }; + }, []); +} +function $71476a6ed7dbbaf3$var$createFocusGuard() { + const element = document.createElement('span'); + element.setAttribute('data-radix-focus-guard', ''); + element.tabIndex = 0; + element.style.cssText = 'outline: none; opacity: 0; position: fixed; pointer-events: none'; + return element; +} +const $71476a6ed7dbbaf3$export$be92b6f5f03c0fe9 = $71476a6ed7dbbaf3$export$ac5b58043b79449b; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-focus-scope/dist/index.js": +/*!***********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-focus-scope/dist/index.js ***! + \***********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $buum9$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $buum9$react = __webpack_require__(/*! react */ "react"); +var $buum9$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $buum9$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $buum9$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "FocusScope", () => $2bc01e66e04aa9ed$export$20e40289641fbbb6); +$parcel$export(module.exports, "Root", () => $2bc01e66e04aa9ed$export$be92b6f5f03c0fe9); +const $2bc01e66e04aa9ed$var$AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount'; +const $2bc01e66e04aa9ed$var$AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount'; +const $2bc01e66e04aa9ed$var$EVENT_OPTIONS = { + bubbles: false, + cancelable: true +}; +/* ------------------------------------------------------------------------------------------------- + * FocusScope + * -----------------------------------------------------------------------------------------------*/ +const $2bc01e66e04aa9ed$var$FOCUS_SCOPE_NAME = 'FocusScope'; +const $2bc01e66e04aa9ed$export$20e40289641fbbb6 = /*#__PURE__*/$buum9$react.forwardRef((props, forwardedRef) => { + const { + loop = false, + trapped = false, + onMountAutoFocus: onMountAutoFocusProp, + onUnmountAutoFocus: onUnmountAutoFocusProp, + ...scopeProps + } = props; + const [container1, setContainer] = $buum9$react.useState(null); + const onMountAutoFocus = $buum9$radixuireactusecallbackref.useCallbackRef(onMountAutoFocusProp); + const onUnmountAutoFocus = $buum9$radixuireactusecallbackref.useCallbackRef(onUnmountAutoFocusProp); + const lastFocusedElementRef = $buum9$react.useRef(null); + const composedRefs = $buum9$radixuireactcomposerefs.useComposedRefs(forwardedRef, node => setContainer(node)); + const focusScope = $buum9$react.useRef({ + paused: false, + pause() { + this.paused = true; + }, + resume() { + this.paused = false; + } + }).current; // Takes care of trapping focus if focus is moved outside programmatically for example + $buum9$react.useEffect(() => { + if (trapped) { + function handleFocusIn(event) { + if (focusScope.paused || !container1) return; + const target = event.target; + if (container1.contains(target)) lastFocusedElementRef.current = target;else $2bc01e66e04aa9ed$var$focus(lastFocusedElementRef.current, { + select: true + }); + } + function handleFocusOut(event) { + if (focusScope.paused || !container1) return; + const relatedTarget = event.relatedTarget; // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases: + // + // 1. When the user switches app/tabs/windows/the browser itself loses focus. + // 2. In Google Chrome, when the focused element is removed from the DOM. + // + // We let the browser do its thing here because: + // + // 1. The browser already keeps a memory of what's focused for when the page gets refocused. + // 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it + // throws the CPU to 100%, so we avoid doing anything for this reason here too. + if (relatedTarget === null) return; // If the focus has moved to an actual legitimate element (`relatedTarget !== null`) + // that is outside the container, we move focus to the last valid focused element inside. + if (!container1.contains(relatedTarget)) $2bc01e66e04aa9ed$var$focus(lastFocusedElementRef.current, { + select: true + }); + } // When the focused element gets removed from the DOM, browsers move focus + // back to the document.body. In this case, we move focus to the container + // to keep focus trapped correctly. + function handleMutations(mutations) { + const focusedElement = document.activeElement; + for (const mutation of mutations) { + if (mutation.removedNodes.length > 0) { + if (!(container1 !== null && container1 !== void 0 && container1.contains(focusedElement))) $2bc01e66e04aa9ed$var$focus(container1); + } + } + } + document.addEventListener('focusin', handleFocusIn); + document.addEventListener('focusout', handleFocusOut); + const mutationObserver = new MutationObserver(handleMutations); + if (container1) mutationObserver.observe(container1, { + childList: true, + subtree: true + }); + return () => { + document.removeEventListener('focusin', handleFocusIn); + document.removeEventListener('focusout', handleFocusOut); + mutationObserver.disconnect(); + }; + } + }, [trapped, container1, focusScope.paused]); + $buum9$react.useEffect(() => { + if (container1) { + $2bc01e66e04aa9ed$var$focusScopesStack.add(focusScope); + const previouslyFocusedElement = document.activeElement; + const hasFocusedCandidate = container1.contains(previouslyFocusedElement); + if (!hasFocusedCandidate) { + const mountEvent = new CustomEvent($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_MOUNT, $2bc01e66e04aa9ed$var$EVENT_OPTIONS); + container1.addEventListener($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_MOUNT, onMountAutoFocus); + container1.dispatchEvent(mountEvent); + if (!mountEvent.defaultPrevented) { + $2bc01e66e04aa9ed$var$focusFirst($2bc01e66e04aa9ed$var$removeLinks($2bc01e66e04aa9ed$var$getTabbableCandidates(container1)), { + select: true + }); + if (document.activeElement === previouslyFocusedElement) $2bc01e66e04aa9ed$var$focus(container1); + } + } + return () => { + container1.removeEventListener($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_MOUNT, onMountAutoFocus); // We hit a react bug (fixed in v17) with focusing in unmount. + // We need to delay the focus a little to get around it for now. + // See: https://github.com/facebook/react/issues/17894 + setTimeout(() => { + const unmountEvent = new CustomEvent($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_UNMOUNT, $2bc01e66e04aa9ed$var$EVENT_OPTIONS); + container1.addEventListener($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); + container1.dispatchEvent(unmountEvent); + if (!unmountEvent.defaultPrevented) $2bc01e66e04aa9ed$var$focus(previouslyFocusedElement !== null && previouslyFocusedElement !== void 0 ? previouslyFocusedElement : document.body, { + select: true + }); + // we need to remove the listener after we `dispatchEvent` + container1.removeEventListener($2bc01e66e04aa9ed$var$AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); + $2bc01e66e04aa9ed$var$focusScopesStack.remove(focusScope); + }, 0); + }; + } + }, [container1, onMountAutoFocus, onUnmountAutoFocus, focusScope]); // Takes care of looping focus (when tabbing whilst at the edges) + const handleKeyDown = $buum9$react.useCallback(event => { + if (!loop && !trapped) return; + if (focusScope.paused) return; + const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey; + const focusedElement = document.activeElement; + if (isTabKey && focusedElement) { + const container = event.currentTarget; + const [first, last] = $2bc01e66e04aa9ed$var$getTabbableEdges(container); + const hasTabbableElementsInside = first && last; // we can only wrap focus if we have tabbable edges + if (!hasTabbableElementsInside) { + if (focusedElement === container) event.preventDefault(); + } else { + if (!event.shiftKey && focusedElement === last) { + event.preventDefault(); + if (loop) $2bc01e66e04aa9ed$var$focus(first, { + select: true + }); + } else if (event.shiftKey && focusedElement === first) { + event.preventDefault(); + if (loop) $2bc01e66e04aa9ed$var$focus(last, { + select: true + }); + } + } + } + }, [loop, trapped, focusScope.paused]); + return /*#__PURE__*/$buum9$react.createElement($buum9$radixuireactprimitive.Primitive.div, $parcel$interopDefault($buum9$babelruntimehelpersextends)({ + tabIndex: -1 + }, scopeProps, { + ref: composedRefs, + onKeyDown: handleKeyDown + })); +}); +/*#__PURE__*/ +Object.assign($2bc01e66e04aa9ed$export$20e40289641fbbb6, { + displayName: $2bc01e66e04aa9ed$var$FOCUS_SCOPE_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * Utils + * -----------------------------------------------------------------------------------------------*/ /** + * Attempts focusing the first element in a list of candidates. + * Stops when focus has actually moved. + */ +function $2bc01e66e04aa9ed$var$focusFirst(candidates) { + let { + select = false + } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const previouslyFocusedElement = document.activeElement; + for (const candidate of candidates) { + $2bc01e66e04aa9ed$var$focus(candidate, { + select: select + }); + if (document.activeElement !== previouslyFocusedElement) return; + } +} +/** + * Returns the first and last tabbable elements inside a container. + */ +function $2bc01e66e04aa9ed$var$getTabbableEdges(container) { + const candidates = $2bc01e66e04aa9ed$var$getTabbableCandidates(container); + const first = $2bc01e66e04aa9ed$var$findVisible(candidates, container); + const last = $2bc01e66e04aa9ed$var$findVisible(candidates.reverse(), container); + return [first, last]; +} +/** + * Returns a list of potential tabbable candidates. + * + * NOTE: This is only a close approximation. For example it doesn't take into account cases like when + * elements are not visible. This cannot be worked out easily by just reading a property, but rather + * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker + * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1 + */ +function $2bc01e66e04aa9ed$var$getTabbableCandidates(container) { + const nodes = []; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, { + acceptNode: node => { + const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden'; + if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP; // `.tabIndex` is not the same as the `tabindex` attribute. It works on the + // runtime's understanding of tabbability, so this automatically accounts + // for any kind of element that could be tabbed to. + return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; + } + }); + while (walker.nextNode()) nodes.push(walker.currentNode); // we do not take into account the order of nodes with positive `tabIndex` as it + // hinders accessibility to have tab order different from visual order. + return nodes; +} +/** + * Returns the first visible element in a list. + * NOTE: Only checks visibility up to the `container`. + */ +function $2bc01e66e04aa9ed$var$findVisible(elements, container) { + for (const element of elements) { + // we stop checking if it's hidden at the `container` level (excluding) + if (!$2bc01e66e04aa9ed$var$isHidden(element, { + upTo: container + })) return element; + } +} +function $2bc01e66e04aa9ed$var$isHidden(node, _ref) { + let { + upTo: upTo + } = _ref; + if (getComputedStyle(node).visibility === 'hidden') return true; + while (node) { + // we stop at `upTo` (excluding it) + if (upTo !== undefined && node === upTo) return false; + if (getComputedStyle(node).display === 'none') return true; + node = node.parentElement; + } + return false; +} +function $2bc01e66e04aa9ed$var$isSelectableInput(element) { + return element instanceof HTMLInputElement && 'select' in element; +} +function $2bc01e66e04aa9ed$var$focus(element) { + let { + select = false + } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + // only focus if that element is focusable + if (element && element.focus) { + const previouslyFocusedElement = document.activeElement; // NOTE: we prevent scrolling on focus, to minimize jarring transitions for users + element.focus({ + preventScroll: true + }); // only select if its not the same element, it supports selection and we need to select + if (element !== previouslyFocusedElement && $2bc01e66e04aa9ed$var$isSelectableInput(element) && select) element.select(); + } +} +/* ------------------------------------------------------------------------------------------------- + * FocusScope stack + * -----------------------------------------------------------------------------------------------*/ +const $2bc01e66e04aa9ed$var$focusScopesStack = $2bc01e66e04aa9ed$var$createFocusScopesStack(); +function $2bc01e66e04aa9ed$var$createFocusScopesStack() { + /** A stack of focus scopes, with the active one at the top */let stack = []; + return { + add(focusScope) { + // pause the currently active focus scope (at the top of the stack) + const activeFocusScope = stack[0]; + if (focusScope !== activeFocusScope) activeFocusScope === null || activeFocusScope === void 0 || activeFocusScope.pause(); + // remove in case it already exists (because we'll re-add it at the top of the stack) + stack = $2bc01e66e04aa9ed$var$arrayRemove(stack, focusScope); + stack.unshift(focusScope); + }, + remove(focusScope) { + var _stack$; + stack = $2bc01e66e04aa9ed$var$arrayRemove(stack, focusScope); + (_stack$ = stack[0]) === null || _stack$ === void 0 || _stack$.resume(); + } + }; +} +function $2bc01e66e04aa9ed$var$arrayRemove(array, item) { + const updatedArray = [...array]; + const index = updatedArray.indexOf(item); + if (index !== -1) updatedArray.splice(index, 1); + return updatedArray; +} +function $2bc01e66e04aa9ed$var$removeLinks(items) { + return items.filter(item => item.tagName !== 'A'); +} +const $2bc01e66e04aa9ed$export$be92b6f5f03c0fe9 = $2bc01e66e04aa9ed$export$20e40289641fbbb6; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-id/dist/index.js": +/*!**************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-id/dist/index.js ***! + \**************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $47woD$react = __webpack_require__(/*! react */ "react"); +var $47woD$radixuireactuselayouteffect = __webpack_require__(/*! @radix-ui/react-use-layout-effect */ "../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useId", () => $dc478e4659f630c5$export$f680877a34711e37); +const $dc478e4659f630c5$var$useReactId = $47woD$react['useId'.toString()] || (() => undefined); +let $dc478e4659f630c5$var$count = 0; +function $dc478e4659f630c5$export$f680877a34711e37(deterministicId) { + const [id, setId] = $47woD$react.useState($dc478e4659f630c5$var$useReactId()); // React versions older than 18 will have client-side ids only. + $47woD$radixuireactuselayouteffect.useLayoutEffect(() => { + if (!deterministicId) setId(reactId => reactId !== null && reactId !== void 0 ? reactId : String($dc478e4659f630c5$var$count++)); + }, [deterministicId]); + return deterministicId || (id ? `radix-${id}` : ''); +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-menu/dist/index.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-menu/dist/index.js ***! + \****************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $cnSS2$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $cnSS2$react = __webpack_require__(/*! react */ "react"); +var $cnSS2$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $cnSS2$radixuireactcollection = __webpack_require__(/*! @radix-ui/react-collection */ "../../../node_modules/@radix-ui/react-collection/dist/index.js"); +var $cnSS2$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $cnSS2$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $cnSS2$radixuireactdirection = __webpack_require__(/*! @radix-ui/react-direction */ "../../../node_modules/@radix-ui/react-direction/dist/index.js"); +var $cnSS2$radixuireactdismissablelayer = __webpack_require__(/*! @radix-ui/react-dismissable-layer */ "../../../node_modules/@radix-ui/react-dismissable-layer/dist/index.js"); +var $cnSS2$radixuireactfocusguards = __webpack_require__(/*! @radix-ui/react-focus-guards */ "../../../node_modules/@radix-ui/react-focus-guards/dist/index.js"); +var $cnSS2$radixuireactfocusscope = __webpack_require__(/*! @radix-ui/react-focus-scope */ "../../../node_modules/@radix-ui/react-focus-scope/dist/index.js"); +var $cnSS2$radixuireactid = __webpack_require__(/*! @radix-ui/react-id */ "../../../node_modules/@radix-ui/react-id/dist/index.js"); +var $cnSS2$radixuireactpopper = __webpack_require__(/*! @radix-ui/react-popper */ "../../../node_modules/@radix-ui/react-popper/dist/index.js"); +var $cnSS2$radixuireactportal = __webpack_require__(/*! @radix-ui/react-portal */ "../../../node_modules/@radix-ui/react-portal/dist/index.js"); +var $cnSS2$radixuireactpresence = __webpack_require__(/*! @radix-ui/react-presence */ "../../../node_modules/@radix-ui/react-presence/dist/index.js"); +var $cnSS2$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $cnSS2$radixuireactrovingfocus = __webpack_require__(/*! @radix-ui/react-roving-focus */ "../../../node_modules/@radix-ui/react-roving-focus/dist/index.js"); +var $cnSS2$radixuireactslot = __webpack_require__(/*! @radix-ui/react-slot */ "../../../node_modules/@radix-ui/react-slot/dist/index.js"); +var $cnSS2$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +var $cnSS2$ariahidden = __webpack_require__(/*! aria-hidden */ "../../../node_modules/aria-hidden/dist/es2015/index.js"); +var $cnSS2$reactremovescroll = __webpack_require__(/*! react-remove-scroll */ "../../../node_modules/react-remove-scroll/dist/es2015/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createMenuScope", () => $213e4d2df823067d$export$4027731b685e72eb); +$parcel$export(module.exports, "Menu", () => $213e4d2df823067d$export$d9b273488cd8ce6f); +$parcel$export(module.exports, "MenuAnchor", () => $213e4d2df823067d$export$9fa5ebd18bee4d43); +$parcel$export(module.exports, "MenuPortal", () => $213e4d2df823067d$export$793392f970497feb); +$parcel$export(module.exports, "MenuContent", () => $213e4d2df823067d$export$479f0f2f71193efe); +$parcel$export(module.exports, "MenuGroup", () => $213e4d2df823067d$export$22a631d1f72787bb); +$parcel$export(module.exports, "MenuLabel", () => $213e4d2df823067d$export$dd37bec0e8a99143); +$parcel$export(module.exports, "MenuItem", () => $213e4d2df823067d$export$2ce376c2cc3355c8); +$parcel$export(module.exports, "MenuCheckboxItem", () => $213e4d2df823067d$export$f6f243521332502d); +$parcel$export(module.exports, "MenuRadioGroup", () => $213e4d2df823067d$export$ea2200c9eee416b3); +$parcel$export(module.exports, "MenuRadioItem", () => $213e4d2df823067d$export$69bd225e9817f6d0); +$parcel$export(module.exports, "MenuItemIndicator", () => $213e4d2df823067d$export$a2593e23056970a3); +$parcel$export(module.exports, "MenuSeparator", () => $213e4d2df823067d$export$1cec7dcdd713e220); +$parcel$export(module.exports, "MenuArrow", () => $213e4d2df823067d$export$bcdda4773debf5fa); +$parcel$export(module.exports, "MenuSub", () => $213e4d2df823067d$export$71bdb9d1e2909932); +$parcel$export(module.exports, "MenuSubTrigger", () => $213e4d2df823067d$export$5fbbb3ba7297405f); +$parcel$export(module.exports, "MenuSubContent", () => $213e4d2df823067d$export$e7142ab31822bde6); +$parcel$export(module.exports, "Root", () => $213e4d2df823067d$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Anchor", () => $213e4d2df823067d$export$b688253958b8dfe7); +$parcel$export(module.exports, "Portal", () => $213e4d2df823067d$export$602eac185826482c); +$parcel$export(module.exports, "Content", () => $213e4d2df823067d$export$7c6e2c02157bb7d2); +$parcel$export(module.exports, "Group", () => $213e4d2df823067d$export$eb2fcfdbd7ba97d4); +$parcel$export(module.exports, "Label", () => $213e4d2df823067d$export$b04be29aa201d4f5); +$parcel$export(module.exports, "Item", () => $213e4d2df823067d$export$6d08773d2e66f8f2); +$parcel$export(module.exports, "CheckboxItem", () => $213e4d2df823067d$export$16ce288f89fa631c); +$parcel$export(module.exports, "RadioGroup", () => $213e4d2df823067d$export$a98f0dcb43a68a25); +$parcel$export(module.exports, "RadioItem", () => $213e4d2df823067d$export$371ab307eab489c0); +$parcel$export(module.exports, "ItemIndicator", () => $213e4d2df823067d$export$c3468e2714d175fa); +$parcel$export(module.exports, "Separator", () => $213e4d2df823067d$export$1ff3c3f08ae963c0); +$parcel$export(module.exports, "Arrow", () => $213e4d2df823067d$export$21b07c8f274aebd5); +$parcel$export(module.exports, "Sub", () => $213e4d2df823067d$export$d7a01e11500dfb6f); +$parcel$export(module.exports, "SubTrigger", () => $213e4d2df823067d$export$2ea8a7a591ac5eac); +$parcel$export(module.exports, "SubContent", () => $213e4d2df823067d$export$6d4de93b380beddf); +const $213e4d2df823067d$var$SELECTION_KEYS = ['Enter', ' ']; +const $213e4d2df823067d$var$FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home']; +const $213e4d2df823067d$var$LAST_KEYS = ['ArrowUp', 'PageDown', 'End']; +const $213e4d2df823067d$var$FIRST_LAST_KEYS = [...$213e4d2df823067d$var$FIRST_KEYS, ...$213e4d2df823067d$var$LAST_KEYS]; +const $213e4d2df823067d$var$SUB_OPEN_KEYS = { + ltr: [...$213e4d2df823067d$var$SELECTION_KEYS, 'ArrowRight'], + rtl: [...$213e4d2df823067d$var$SELECTION_KEYS, 'ArrowLeft'] +}; +const $213e4d2df823067d$var$SUB_CLOSE_KEYS = { + ltr: ['ArrowLeft'], + rtl: ['ArrowRight'] +}; +/* ------------------------------------------------------------------------------------------------- + * Menu + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$MENU_NAME = 'Menu'; +const [$213e4d2df823067d$var$Collection, $213e4d2df823067d$var$useCollection, $213e4d2df823067d$var$createCollectionScope] = $cnSS2$radixuireactcollection.createCollection($213e4d2df823067d$var$MENU_NAME); +const [$213e4d2df823067d$var$createMenuContext, $213e4d2df823067d$export$4027731b685e72eb] = $cnSS2$radixuireactcontext.createContextScope($213e4d2df823067d$var$MENU_NAME, [$213e4d2df823067d$var$createCollectionScope, $cnSS2$radixuireactpopper.createPopperScope, $cnSS2$radixuireactrovingfocus.createRovingFocusGroupScope]); +const $213e4d2df823067d$var$usePopperScope = $cnSS2$radixuireactpopper.createPopperScope(); +const $213e4d2df823067d$var$useRovingFocusGroupScope = $cnSS2$radixuireactrovingfocus.createRovingFocusGroupScope(); +const [$213e4d2df823067d$var$MenuProvider, $213e4d2df823067d$var$useMenuContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$MENU_NAME); +const [$213e4d2df823067d$var$MenuRootProvider, $213e4d2df823067d$var$useMenuRootContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$MENU_NAME); +const $213e4d2df823067d$export$d9b273488cd8ce6f = props => { + const { + __scopeMenu: __scopeMenu, + open = false, + children: children, + dir: dir, + onOpenChange: onOpenChange, + modal = true + } = props; + const popperScope = $213e4d2df823067d$var$usePopperScope(__scopeMenu); + const [content, setContent] = $cnSS2$react.useState(null); + const isUsingKeyboardRef = $cnSS2$react.useRef(false); + const handleOpenChange = $cnSS2$radixuireactusecallbackref.useCallbackRef(onOpenChange); + const direction = $cnSS2$radixuireactdirection.useDirection(dir); + $cnSS2$react.useEffect(() => { + // Capture phase ensures we set the boolean before any side effects execute + // in response to the key or pointer event as they might depend on this value. + const handleKeyDown = () => { + isUsingKeyboardRef.current = true; + document.addEventListener('pointerdown', handlePointer, { + capture: true, + once: true + }); + document.addEventListener('pointermove', handlePointer, { + capture: true, + once: true + }); + }; + const handlePointer = () => isUsingKeyboardRef.current = false; + document.addEventListener('keydown', handleKeyDown, { + capture: true + }); + return () => { + document.removeEventListener('keydown', handleKeyDown, { + capture: true + }); + document.removeEventListener('pointerdown', handlePointer, { + capture: true + }); + document.removeEventListener('pointermove', handlePointer, { + capture: true + }); + }; + }, []); + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpopper.Root, popperScope, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuProvider, { + scope: __scopeMenu, + open: open, + onOpenChange: handleOpenChange, + content: content, + onContentChange: setContent + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuRootProvider, { + scope: __scopeMenu, + onClose: $cnSS2$react.useCallback(() => handleOpenChange(false), [handleOpenChange]), + isUsingKeyboardRef: isUsingKeyboardRef, + dir: direction, + modal: modal + }, children))); +}; +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$d9b273488cd8ce6f, { + displayName: $213e4d2df823067d$var$MENU_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuAnchor + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$ANCHOR_NAME = 'MenuAnchor'; +const $213e4d2df823067d$export$9fa5ebd18bee4d43 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + ...anchorProps + } = props; + const popperScope = $213e4d2df823067d$var$usePopperScope(__scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpopper.Anchor, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, popperScope, anchorProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$9fa5ebd18bee4d43, { + displayName: $213e4d2df823067d$var$ANCHOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuPortal + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$PORTAL_NAME = 'MenuPortal'; +const [$213e4d2df823067d$var$PortalProvider, $213e4d2df823067d$var$usePortalContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$PORTAL_NAME, { + forceMount: undefined +}); +const $213e4d2df823067d$export$793392f970497feb = props => { + const { + __scopeMenu: __scopeMenu, + forceMount: forceMount, + children: children, + container: container + } = props; + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$PORTAL_NAME, __scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$PortalProvider, { + scope: __scopeMenu, + forceMount: forceMount + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactportal.Portal, { + asChild: true, + container: container + }, children))); +}; +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$793392f970497feb, { + displayName: $213e4d2df823067d$var$PORTAL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuContent + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$CONTENT_NAME = 'MenuContent'; +const [$213e4d2df823067d$var$MenuContentProvider, $213e4d2df823067d$var$useMenuContentContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$CONTENT_NAME); +const $213e4d2df823067d$export$479f0f2f71193efe = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const portalContext = $213e4d2df823067d$var$usePortalContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const { + forceMount = portalContext.forceMount, + ...contentProps + } = props; + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const rootContext = $213e4d2df823067d$var$useMenuRootContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$Collection.Provider, { + scope: props.__scopeMenu + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$Collection.Slot, { + scope: props.__scopeMenu + }, rootContext.modal ? /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuRootContentModal, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, contentProps, { + ref: forwardedRef + })) : /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuRootContentNonModal, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, contentProps, { + ref: forwardedRef + }))))); +}); +/* ---------------------------------------------------------------------------------------------- */ +const $213e4d2df823067d$var$MenuRootContentModal = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const ref = $cnSS2$react.useRef(null); + const composedRefs = $cnSS2$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); // Hide everything from ARIA except the `MenuContent` + $cnSS2$react.useEffect(() => { + const content = ref.current; + if (content) return $cnSS2$ariahidden.hideOthers(content); + }, []); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuContentImpl, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, props, { + ref: composedRefs // we make sure we're not trapping once it's been closed + , + + trapFocus: context.open // make sure to only disable pointer events when open + , + + disableOutsidePointerEvents: context.open, + disableOutsideScroll: true // When focus is trapped, a `focusout` event may still happen. + , + + onFocusOutside: $cnSS2$radixuiprimitive.composeEventHandlers(props.onFocusOutside, event => event.preventDefault(), { + checkForDefaultPrevented: false + }), + onDismiss: () => context.onOpenChange(false) + })); +}); +const $213e4d2df823067d$var$MenuRootContentNonModal = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuContentImpl, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, props, { + ref: forwardedRef, + trapFocus: false, + disableOutsidePointerEvents: false, + disableOutsideScroll: false, + onDismiss: () => context.onOpenChange(false) + })); +}); +/* ---------------------------------------------------------------------------------------------- */ +const $213e4d2df823067d$var$MenuContentImpl = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + loop = false, + trapFocus: trapFocus, + onOpenAutoFocus: onOpenAutoFocus, + onCloseAutoFocus: onCloseAutoFocus, + disableOutsidePointerEvents: disableOutsidePointerEvents, + onEntryFocus: onEntryFocus, + onEscapeKeyDown: onEscapeKeyDown, + onPointerDownOutside: onPointerDownOutside, + onFocusOutside: onFocusOutside, + onInteractOutside: onInteractOutside, + onDismiss: onDismiss, + disableOutsideScroll: disableOutsideScroll, + ...contentProps + } = props; + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$CONTENT_NAME, __scopeMenu); + const rootContext = $213e4d2df823067d$var$useMenuRootContext($213e4d2df823067d$var$CONTENT_NAME, __scopeMenu); + const popperScope = $213e4d2df823067d$var$usePopperScope(__scopeMenu); + const rovingFocusGroupScope = $213e4d2df823067d$var$useRovingFocusGroupScope(__scopeMenu); + const getItems = $213e4d2df823067d$var$useCollection(__scopeMenu); + const [currentItemId, setCurrentItemId] = $cnSS2$react.useState(null); + const contentRef = $cnSS2$react.useRef(null); + const composedRefs = $cnSS2$radixuireactcomposerefs.useComposedRefs(forwardedRef, contentRef, context.onContentChange); + const timerRef = $cnSS2$react.useRef(0); + const searchRef = $cnSS2$react.useRef(''); + const pointerGraceTimerRef = $cnSS2$react.useRef(0); + const pointerGraceIntentRef = $cnSS2$react.useRef(null); + const pointerDirRef = $cnSS2$react.useRef('right'); + const lastPointerXRef = $cnSS2$react.useRef(0); + const ScrollLockWrapper = disableOutsideScroll ? $cnSS2$reactremovescroll.RemoveScroll : $cnSS2$react.Fragment; + const scrollLockWrapperProps = disableOutsideScroll ? { + as: $cnSS2$radixuireactslot.Slot, + allowPinchZoom: true + } : undefined; + const handleTypeaheadSearch = key => { + var _items$find, _items$find2; + const search = searchRef.current + key; + const items = getItems().filter(item => !item.disabled); + const currentItem = document.activeElement; + const currentMatch = (_items$find = items.find(item => item.ref.current === currentItem)) === null || _items$find === void 0 ? void 0 : _items$find.textValue; + const values = items.map(item => item.textValue); + const nextMatch = $213e4d2df823067d$var$getNextMatch(values, search, currentMatch); + const newItem = (_items$find2 = items.find(item => item.textValue === nextMatch)) === null || _items$find2 === void 0 ? void 0 : _items$find2.ref.current; // Reset `searchRef` 1 second after it was last updated + (function updateSearch(value) { + searchRef.current = value; + window.clearTimeout(timerRef.current); + if (value !== '') timerRef.current = window.setTimeout(() => updateSearch(''), 1000); + })(search); + if (newItem) + /** + * Imperative focus during keydown is risky so we prevent React's batching updates + * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332 + */ + setTimeout(() => newItem.focus()); + }; + $cnSS2$react.useEffect(() => { + return () => window.clearTimeout(timerRef.current); + }, []); // Make sure the whole tree has focus guards as our `MenuContent` may be + // the last element in the DOM (beacuse of the `Portal`) + $cnSS2$radixuireactfocusguards.useFocusGuards(); + const isPointerMovingToSubmenu = $cnSS2$react.useCallback(event => { + var _pointerGraceIntentRe, _pointerGraceIntentRe2; + const isMovingTowards = pointerDirRef.current === ((_pointerGraceIntentRe = pointerGraceIntentRef.current) === null || _pointerGraceIntentRe === void 0 ? void 0 : _pointerGraceIntentRe.side); + return isMovingTowards && $213e4d2df823067d$var$isPointerInGraceArea(event, (_pointerGraceIntentRe2 = pointerGraceIntentRef.current) === null || _pointerGraceIntentRe2 === void 0 ? void 0 : _pointerGraceIntentRe2.area); + }, []); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuContentProvider, { + scope: __scopeMenu, + searchRef: searchRef, + onItemEnter: $cnSS2$react.useCallback(event => { + if (isPointerMovingToSubmenu(event)) event.preventDefault(); + }, [isPointerMovingToSubmenu]), + onItemLeave: $cnSS2$react.useCallback(event => { + var _contentRef$current; + if (isPointerMovingToSubmenu(event)) return; + (_contentRef$current = contentRef.current) === null || _contentRef$current === void 0 || _contentRef$current.focus(); + setCurrentItemId(null); + }, [isPointerMovingToSubmenu]), + onTriggerLeave: $cnSS2$react.useCallback(event => { + if (isPointerMovingToSubmenu(event)) event.preventDefault(); + }, [isPointerMovingToSubmenu]), + pointerGraceTimerRef: pointerGraceTimerRef, + onPointerGraceIntentChange: $cnSS2$react.useCallback(intent => { + pointerGraceIntentRef.current = intent; + }, []) + }, /*#__PURE__*/$cnSS2$react.createElement(ScrollLockWrapper, scrollLockWrapperProps, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactfocusscope.FocusScope, { + asChild: true, + trapped: trapFocus, + onMountAutoFocus: $cnSS2$radixuiprimitive.composeEventHandlers(onOpenAutoFocus, event => { + var _contentRef$current2; + // when opening, explicitly focus the content area only and leave + // `onEntryFocus` in control of focusing first item + event.preventDefault(); + (_contentRef$current2 = contentRef.current) === null || _contentRef$current2 === void 0 || _contentRef$current2.focus(); + }), + onUnmountAutoFocus: onCloseAutoFocus + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactdismissablelayer.DismissableLayer, { + asChild: true, + disableOutsidePointerEvents: disableOutsidePointerEvents, + onEscapeKeyDown: onEscapeKeyDown, + onPointerDownOutside: onPointerDownOutside, + onFocusOutside: onFocusOutside, + onInteractOutside: onInteractOutside, + onDismiss: onDismiss + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactrovingfocus.Root, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + asChild: true + }, rovingFocusGroupScope, { + dir: rootContext.dir, + orientation: "vertical", + loop: loop, + currentTabStopId: currentItemId, + onCurrentTabStopIdChange: setCurrentItemId, + onEntryFocus: $cnSS2$radixuiprimitive.composeEventHandlers(onEntryFocus, event => { + // only focus first item when using keyboard + if (!rootContext.isUsingKeyboardRef.current) event.preventDefault(); + }) + }), /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpopper.Content, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "menu", + "aria-orientation": "vertical", + "data-state": $213e4d2df823067d$var$getOpenState(context.open), + "data-radix-menu-content": "", + dir: rootContext.dir + }, popperScope, contentProps, { + ref: composedRefs, + style: { + outline: 'none', + ...contentProps.style + }, + onKeyDown: $cnSS2$radixuiprimitive.composeEventHandlers(contentProps.onKeyDown, event => { + // submenu key events bubble through portals. We only care about keys in this menu. + const target = event.target; + const isKeyDownInside = target.closest('[data-radix-menu-content]') === event.currentTarget; + const isModifierKey = event.ctrlKey || event.altKey || event.metaKey; + const isCharacterKey = event.key.length === 1; + if (isKeyDownInside) { + // menus should not be navigated using tab key so we prevent it + if (event.key === 'Tab') event.preventDefault(); + if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key); + } // focus first/last item based on key pressed + const content = contentRef.current; + if (event.target !== content) return; + if (!$213e4d2df823067d$var$FIRST_LAST_KEYS.includes(event.key)) return; + event.preventDefault(); + const items = getItems().filter(item => !item.disabled); + const candidateNodes = items.map(item => item.ref.current); + if ($213e4d2df823067d$var$LAST_KEYS.includes(event.key)) candidateNodes.reverse(); + $213e4d2df823067d$var$focusFirst(candidateNodes); + }), + onBlur: $cnSS2$radixuiprimitive.composeEventHandlers(props.onBlur, event => { + // clear search buffer when leaving the menu + if (!event.currentTarget.contains(event.target)) { + window.clearTimeout(timerRef.current); + searchRef.current = ''; + } + }), + onPointerMove: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerMove, $213e4d2df823067d$var$whenMouse(event => { + const target = event.target; + const pointerXHasChanged = lastPointerXRef.current !== event.clientX; // We don't use `event.movementX` for this check because Safari will + // always return `0` on a pointer event. + if (event.currentTarget.contains(target) && pointerXHasChanged) { + const newDir = event.clientX > lastPointerXRef.current ? 'right' : 'left'; + pointerDirRef.current = newDir; + lastPointerXRef.current = event.clientX; + } + })) + }))))))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$479f0f2f71193efe, { + displayName: $213e4d2df823067d$var$CONTENT_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuGroup + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$GROUP_NAME = 'MenuGroup'; +const $213e4d2df823067d$export$22a631d1f72787bb = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + ...groupProps + } = props; + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactprimitive.Primitive.div, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "group" + }, groupProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$22a631d1f72787bb, { + displayName: $213e4d2df823067d$var$GROUP_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuLabel + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$LABEL_NAME = 'MenuLabel'; +const $213e4d2df823067d$export$dd37bec0e8a99143 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + ...labelProps + } = props; + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactprimitive.Primitive.div, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, labelProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$dd37bec0e8a99143, { + displayName: $213e4d2df823067d$var$LABEL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuItem + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$ITEM_NAME = 'MenuItem'; +const $213e4d2df823067d$var$ITEM_SELECT = 'menu.itemSelect'; +const $213e4d2df823067d$export$2ce376c2cc3355c8 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + disabled = false, + onSelect: onSelect, + ...itemProps + } = props; + const ref = $cnSS2$react.useRef(null); + const rootContext = $213e4d2df823067d$var$useMenuRootContext($213e4d2df823067d$var$ITEM_NAME, props.__scopeMenu); + const contentContext = $213e4d2df823067d$var$useMenuContentContext($213e4d2df823067d$var$ITEM_NAME, props.__scopeMenu); + const composedRefs = $cnSS2$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + const isPointerDownRef = $cnSS2$react.useRef(false); + const handleSelect = () => { + const menuItem = ref.current; + if (!disabled && menuItem) { + const itemSelectEvent = new CustomEvent($213e4d2df823067d$var$ITEM_SELECT, { + bubbles: true, + cancelable: true + }); + menuItem.addEventListener($213e4d2df823067d$var$ITEM_SELECT, event => onSelect === null || onSelect === void 0 ? void 0 : onSelect(event), { + once: true + }); + $cnSS2$radixuireactprimitive.dispatchDiscreteCustomEvent(menuItem, itemSelectEvent); + if (itemSelectEvent.defaultPrevented) isPointerDownRef.current = false;else rootContext.onClose(); + } + }; + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuItemImpl, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, itemProps, { + ref: composedRefs, + disabled: disabled, + onClick: $cnSS2$radixuiprimitive.composeEventHandlers(props.onClick, handleSelect), + onPointerDown: event => { + var _props$onPointerDown; + (_props$onPointerDown = props.onPointerDown) === null || _props$onPointerDown === void 0 || _props$onPointerDown.call(props, event); + isPointerDownRef.current = true; + }, + onPointerUp: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerUp, event => { + var _event$currentTarget; + // Pointer down can move to a different menu item which should activate it on pointer up. + // We dispatch a click for selection to allow composition with click based triggers and to + // prevent Firefox from getting stuck in text selection mode when the menu closes. + if (!isPointerDownRef.current) (_event$currentTarget = event.currentTarget) === null || _event$currentTarget === void 0 || _event$currentTarget.click(); + }), + onKeyDown: $cnSS2$radixuiprimitive.composeEventHandlers(props.onKeyDown, event => { + const isTypingAhead = contentContext.searchRef.current !== ''; + if (disabled || isTypingAhead && event.key === ' ') return; + if ($213e4d2df823067d$var$SELECTION_KEYS.includes(event.key)) { + event.currentTarget.click(); + /** + * We prevent default browser behaviour for selection keys as they should trigger + * a selection only: + * - prevents space from scrolling the page. + * - if keydown causes focus to move, prevents keydown from firing on the new target. + */ + event.preventDefault(); + } + }) + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$2ce376c2cc3355c8, { + displayName: $213e4d2df823067d$var$ITEM_NAME +}); +/* ---------------------------------------------------------------------------------------------- */ +const $213e4d2df823067d$var$MenuItemImpl = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + disabled = false, + textValue: textValue, + ...itemProps + } = props; + const contentContext = $213e4d2df823067d$var$useMenuContentContext($213e4d2df823067d$var$ITEM_NAME, __scopeMenu); + const rovingFocusGroupScope = $213e4d2df823067d$var$useRovingFocusGroupScope(__scopeMenu); + const ref = $cnSS2$react.useRef(null); + const composedRefs = $cnSS2$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + const [isFocused, setIsFocused] = $cnSS2$react.useState(false); // get the item's `.textContent` as default strategy for typeahead `textValue` + const [textContent, setTextContent] = $cnSS2$react.useState(''); + $cnSS2$react.useEffect(() => { + const menuItem = ref.current; + if (menuItem) { + var _menuItem$textContent; + setTextContent(((_menuItem$textContent = menuItem.textContent) !== null && _menuItem$textContent !== void 0 ? _menuItem$textContent : '').trim()); + } + }, [itemProps.children]); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$Collection.ItemSlot, { + scope: __scopeMenu, + disabled: disabled, + textValue: textValue !== null && textValue !== void 0 ? textValue : textContent + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactrovingfocus.Item, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + asChild: true + }, rovingFocusGroupScope, { + focusable: !disabled + }), /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactprimitive.Primitive.div, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "menuitem", + "data-highlighted": isFocused ? '' : undefined, + "aria-disabled": disabled || undefined, + "data-disabled": disabled ? '' : undefined + }, itemProps, { + ref: composedRefs, + onPointerMove: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerMove, $213e4d2df823067d$var$whenMouse(event => { + if (disabled) contentContext.onItemLeave(event);else { + contentContext.onItemEnter(event); + if (!event.defaultPrevented) { + const item = event.currentTarget; + item.focus(); + } + } + })), + onPointerLeave: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerLeave, $213e4d2df823067d$var$whenMouse(event => contentContext.onItemLeave(event))), + onFocus: $cnSS2$radixuiprimitive.composeEventHandlers(props.onFocus, () => setIsFocused(true)), + onBlur: $cnSS2$radixuiprimitive.composeEventHandlers(props.onBlur, () => setIsFocused(false)) + })))); +}); +/* ------------------------------------------------------------------------------------------------- + * MenuCheckboxItem + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$CHECKBOX_ITEM_NAME = 'MenuCheckboxItem'; +const $213e4d2df823067d$export$f6f243521332502d = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + checked = false, + onCheckedChange: onCheckedChange, + ...checkboxItemProps + } = props; + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$ItemIndicatorProvider, { + scope: props.__scopeMenu, + checked: checked + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$export$2ce376c2cc3355c8, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "menuitemcheckbox", + "aria-checked": $213e4d2df823067d$var$isIndeterminate(checked) ? 'mixed' : checked + }, checkboxItemProps, { + ref: forwardedRef, + "data-state": $213e4d2df823067d$var$getCheckedState(checked), + onSelect: $cnSS2$radixuiprimitive.composeEventHandlers(checkboxItemProps.onSelect, () => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange($213e4d2df823067d$var$isIndeterminate(checked) ? true : !checked), { + checkForDefaultPrevented: false + }) + }))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$f6f243521332502d, { + displayName: $213e4d2df823067d$var$CHECKBOX_ITEM_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuRadioGroup + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$RADIO_GROUP_NAME = 'MenuRadioGroup'; +const [$213e4d2df823067d$var$RadioGroupProvider, $213e4d2df823067d$var$useRadioGroupContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$RADIO_GROUP_NAME, { + value: undefined, + onValueChange: () => {} +}); +const $213e4d2df823067d$export$ea2200c9eee416b3 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + value: value, + onValueChange: onValueChange, + ...groupProps + } = props; + const handleValueChange = $cnSS2$radixuireactusecallbackref.useCallbackRef(onValueChange); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$RadioGroupProvider, { + scope: props.__scopeMenu, + value: value, + onValueChange: handleValueChange + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$export$22a631d1f72787bb, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, groupProps, { + ref: forwardedRef + }))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$ea2200c9eee416b3, { + displayName: $213e4d2df823067d$var$RADIO_GROUP_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuRadioItem + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$RADIO_ITEM_NAME = 'MenuRadioItem'; +const $213e4d2df823067d$export$69bd225e9817f6d0 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + value: value, + ...radioItemProps + } = props; + const context = $213e4d2df823067d$var$useRadioGroupContext($213e4d2df823067d$var$RADIO_ITEM_NAME, props.__scopeMenu); + const checked = value === context.value; + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$ItemIndicatorProvider, { + scope: props.__scopeMenu, + checked: checked + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$export$2ce376c2cc3355c8, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "menuitemradio", + "aria-checked": checked + }, radioItemProps, { + ref: forwardedRef, + "data-state": $213e4d2df823067d$var$getCheckedState(checked), + onSelect: $cnSS2$radixuiprimitive.composeEventHandlers(radioItemProps.onSelect, () => { + var _context$onValueChang; + return (_context$onValueChang = context.onValueChange) === null || _context$onValueChang === void 0 ? void 0 : _context$onValueChang.call(context, value); + }, { + checkForDefaultPrevented: false + }) + }))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$69bd225e9817f6d0, { + displayName: $213e4d2df823067d$var$RADIO_ITEM_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuItemIndicator + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$ITEM_INDICATOR_NAME = 'MenuItemIndicator'; +const [$213e4d2df823067d$var$ItemIndicatorProvider, $213e4d2df823067d$var$useItemIndicatorContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$ITEM_INDICATOR_NAME, { + checked: false +}); +const $213e4d2df823067d$export$a2593e23056970a3 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + forceMount: forceMount, + ...itemIndicatorProps + } = props; + const indicatorContext = $213e4d2df823067d$var$useItemIndicatorContext($213e4d2df823067d$var$ITEM_INDICATOR_NAME, __scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpresence.Presence, { + present: forceMount || $213e4d2df823067d$var$isIndeterminate(indicatorContext.checked) || indicatorContext.checked === true + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactprimitive.Primitive.span, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, itemIndicatorProps, { + ref: forwardedRef, + "data-state": $213e4d2df823067d$var$getCheckedState(indicatorContext.checked) + }))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$a2593e23056970a3, { + displayName: $213e4d2df823067d$var$ITEM_INDICATOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuSeparator + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$SEPARATOR_NAME = 'MenuSeparator'; +const $213e4d2df823067d$export$1cec7dcdd713e220 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + ...separatorProps + } = props; + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactprimitive.Primitive.div, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + role: "separator", + "aria-orientation": "horizontal" + }, separatorProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$1cec7dcdd713e220, { + displayName: $213e4d2df823067d$var$SEPARATOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuArrow + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$ARROW_NAME = 'MenuArrow'; +const $213e4d2df823067d$export$bcdda4773debf5fa = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const { + __scopeMenu: __scopeMenu, + ...arrowProps + } = props; + const popperScope = $213e4d2df823067d$var$usePopperScope(__scopeMenu); + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpopper.Arrow, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({}, popperScope, arrowProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$bcdda4773debf5fa, { + displayName: $213e4d2df823067d$var$ARROW_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuSub + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$SUB_NAME = 'MenuSub'; +const [$213e4d2df823067d$var$MenuSubProvider, $213e4d2df823067d$var$useMenuSubContext] = $213e4d2df823067d$var$createMenuContext($213e4d2df823067d$var$SUB_NAME); +const $213e4d2df823067d$export$71bdb9d1e2909932 = props => { + const { + __scopeMenu: __scopeMenu, + children: children, + open = false, + onOpenChange: onOpenChange + } = props; + const parentMenuContext = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$SUB_NAME, __scopeMenu); + const popperScope = $213e4d2df823067d$var$usePopperScope(__scopeMenu); + const [trigger, setTrigger] = $cnSS2$react.useState(null); + const [content, setContent] = $cnSS2$react.useState(null); + const handleOpenChange = $cnSS2$radixuireactusecallbackref.useCallbackRef(onOpenChange); // Prevent the parent menu from reopening with open submenus. + $cnSS2$react.useEffect(() => { + if (parentMenuContext.open === false) handleOpenChange(false); + return () => handleOpenChange(false); + }, [parentMenuContext.open, handleOpenChange]); + return /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpopper.Root, popperScope, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuProvider, { + scope: __scopeMenu, + open: open, + onOpenChange: handleOpenChange, + content: content, + onContentChange: setContent + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuSubProvider, { + scope: __scopeMenu, + contentId: $cnSS2$radixuireactid.useId(), + triggerId: $cnSS2$radixuireactid.useId(), + trigger: trigger, + onTriggerChange: setTrigger + }, children))); +}; +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$71bdb9d1e2909932, { + displayName: $213e4d2df823067d$var$SUB_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuSubTrigger + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$SUB_TRIGGER_NAME = 'MenuSubTrigger'; +const $213e4d2df823067d$export$5fbbb3ba7297405f = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$SUB_TRIGGER_NAME, props.__scopeMenu); + const rootContext = $213e4d2df823067d$var$useMenuRootContext($213e4d2df823067d$var$SUB_TRIGGER_NAME, props.__scopeMenu); + const subContext = $213e4d2df823067d$var$useMenuSubContext($213e4d2df823067d$var$SUB_TRIGGER_NAME, props.__scopeMenu); + const contentContext = $213e4d2df823067d$var$useMenuContentContext($213e4d2df823067d$var$SUB_TRIGGER_NAME, props.__scopeMenu); + const openTimerRef = $cnSS2$react.useRef(null); + const { + pointerGraceTimerRef: pointerGraceTimerRef, + onPointerGraceIntentChange: onPointerGraceIntentChange + } = contentContext; + const scope = { + __scopeMenu: props.__scopeMenu + }; + const clearOpenTimer = $cnSS2$react.useCallback(() => { + if (openTimerRef.current) window.clearTimeout(openTimerRef.current); + openTimerRef.current = null; + }, []); + $cnSS2$react.useEffect(() => clearOpenTimer, [clearOpenTimer]); + $cnSS2$react.useEffect(() => { + const pointerGraceTimer = pointerGraceTimerRef.current; + return () => { + window.clearTimeout(pointerGraceTimer); + onPointerGraceIntentChange(null); + }; + }, [pointerGraceTimerRef, onPointerGraceIntentChange]); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$export$9fa5ebd18bee4d43, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + asChild: true + }, scope), /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuItemImpl, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + id: subContext.triggerId, + "aria-haspopup": "menu", + "aria-expanded": context.open, + "aria-controls": subContext.contentId, + "data-state": $213e4d2df823067d$var$getOpenState(context.open) + }, props, { + ref: $cnSS2$radixuireactcomposerefs.composeRefs(forwardedRef, subContext.onTriggerChange) // This is redundant for mouse users but we cannot determine pointer type from + , + + onClick: event => { + var _props$onClick; + (_props$onClick = props.onClick) === null || _props$onClick === void 0 || _props$onClick.call(props, event); + if (props.disabled || event.defaultPrevented) return; + /** + * We manually focus because iOS Safari doesn't always focus on click (e.g. buttons) + * and we rely heavily on `onFocusOutside` for submenus to close when switching + * between separate submenus. + */ + event.currentTarget.focus(); + if (!context.open) context.onOpenChange(true); + }, + onPointerMove: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerMove, $213e4d2df823067d$var$whenMouse(event => { + contentContext.onItemEnter(event); + if (event.defaultPrevented) return; + if (!props.disabled && !context.open && !openTimerRef.current) { + contentContext.onPointerGraceIntentChange(null); + openTimerRef.current = window.setTimeout(() => { + context.onOpenChange(true); + clearOpenTimer(); + }, 100); + } + })), + onPointerLeave: $cnSS2$radixuiprimitive.composeEventHandlers(props.onPointerLeave, $213e4d2df823067d$var$whenMouse(event => { + var _context$content; + clearOpenTimer(); + const contentRect = (_context$content = context.content) === null || _context$content === void 0 ? void 0 : _context$content.getBoundingClientRect(); + if (contentRect) { + var _context$content2; + // TODO: make sure to update this when we change positioning logic + const side = (_context$content2 = context.content) === null || _context$content2 === void 0 ? void 0 : _context$content2.dataset.side; + const rightSide = side === 'right'; + const bleed = rightSide ? -5 : 5; + const contentNearEdge = contentRect[rightSide ? 'left' : 'right']; + const contentFarEdge = contentRect[rightSide ? 'right' : 'left']; + contentContext.onPointerGraceIntentChange({ + area: [ + // consistently within polygon bounds + { + x: event.clientX + bleed, + y: event.clientY + }, { + x: contentNearEdge, + y: contentRect.top + }, { + x: contentFarEdge, + y: contentRect.top + }, { + x: contentFarEdge, + y: contentRect.bottom + }, { + x: contentNearEdge, + y: contentRect.bottom + }], + side: side + }); + window.clearTimeout(pointerGraceTimerRef.current); + pointerGraceTimerRef.current = window.setTimeout(() => contentContext.onPointerGraceIntentChange(null), 300); + } else { + contentContext.onTriggerLeave(event); + if (event.defaultPrevented) return; // There's 100ms where the user may leave an item before the submenu was opened. + contentContext.onPointerGraceIntentChange(null); + } + })), + onKeyDown: $cnSS2$radixuiprimitive.composeEventHandlers(props.onKeyDown, event => { + const isTypingAhead = contentContext.searchRef.current !== ''; + if (props.disabled || isTypingAhead && event.key === ' ') return; + if ($213e4d2df823067d$var$SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) { + var _context$content3; + context.onOpenChange(true); // The trigger may hold focus if opened via pointer interaction + // so we ensure content is given focus again when switching to keyboard. + (_context$content3 = context.content) === null || _context$content3 === void 0 || _context$content3.focus(); // prevent window from scrolling + event.preventDefault(); + } + }) + }))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$5fbbb3ba7297405f, { + displayName: $213e4d2df823067d$var$SUB_TRIGGER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * MenuSubContent + * -----------------------------------------------------------------------------------------------*/ +const $213e4d2df823067d$var$SUB_CONTENT_NAME = 'MenuSubContent'; +const $213e4d2df823067d$export$e7142ab31822bde6 = /*#__PURE__*/$cnSS2$react.forwardRef((props, forwardedRef) => { + const portalContext = $213e4d2df823067d$var$usePortalContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const { + forceMount = portalContext.forceMount, + ...subContentProps + } = props; + const context = $213e4d2df823067d$var$useMenuContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const rootContext = $213e4d2df823067d$var$useMenuRootContext($213e4d2df823067d$var$CONTENT_NAME, props.__scopeMenu); + const subContext = $213e4d2df823067d$var$useMenuSubContext($213e4d2df823067d$var$SUB_CONTENT_NAME, props.__scopeMenu); + const ref = $cnSS2$react.useRef(null); + const composedRefs = $cnSS2$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + return /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$Collection.Provider, { + scope: props.__scopeMenu + }, /*#__PURE__*/$cnSS2$react.createElement($cnSS2$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$Collection.Slot, { + scope: props.__scopeMenu + }, /*#__PURE__*/$cnSS2$react.createElement($213e4d2df823067d$var$MenuContentImpl, $parcel$interopDefault($cnSS2$babelruntimehelpersextends)({ + id: subContext.contentId, + "aria-labelledby": subContext.triggerId + }, subContentProps, { + ref: composedRefs, + align: "start", + side: rootContext.dir === 'rtl' ? 'left' : 'right', + disableOutsidePointerEvents: false, + disableOutsideScroll: false, + trapFocus: false, + onOpenAutoFocus: event => { + var _ref$current; + // when opening a submenu, focus content for keyboard users only + if (rootContext.isUsingKeyboardRef.current) (_ref$current = ref.current) === null || _ref$current === void 0 || _ref$current.focus(); + event.preventDefault(); + } // The menu might close because of focusing another menu item in the parent menu. We + , + + onCloseAutoFocus: event => event.preventDefault(), + onFocusOutside: $cnSS2$radixuiprimitive.composeEventHandlers(props.onFocusOutside, event => { + // We prevent closing when the trigger is focused to avoid triggering a re-open animation + // on pointer interaction. + if (event.target !== subContext.trigger) context.onOpenChange(false); + }), + onEscapeKeyDown: $cnSS2$radixuiprimitive.composeEventHandlers(props.onEscapeKeyDown, event => { + rootContext.onClose(); // ensure pressing escape in submenu doesn't escape full screen mode + event.preventDefault(); + }), + onKeyDown: $cnSS2$radixuiprimitive.composeEventHandlers(props.onKeyDown, event => { + // Submenu key events bubble through portals. We only care about keys in this menu. + const isKeyDownInside = event.currentTarget.contains(event.target); + const isCloseKey = $213e4d2df823067d$var$SUB_CLOSE_KEYS[rootContext.dir].includes(event.key); + if (isKeyDownInside && isCloseKey) { + var _subContext$trigger; + context.onOpenChange(false); // We focus manually because we prevented it in `onCloseAutoFocus` + (_subContext$trigger = subContext.trigger) === null || _subContext$trigger === void 0 || _subContext$trigger.focus(); // prevent window from scrolling + event.preventDefault(); + } + }) + }))))); +}); +/*#__PURE__*/ +Object.assign($213e4d2df823067d$export$e7142ab31822bde6, { + displayName: $213e4d2df823067d$var$SUB_CONTENT_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +function $213e4d2df823067d$var$getOpenState(open) { + return open ? 'open' : 'closed'; +} +function $213e4d2df823067d$var$isIndeterminate(checked) { + return checked === 'indeterminate'; +} +function $213e4d2df823067d$var$getCheckedState(checked) { + return $213e4d2df823067d$var$isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked'; +} +function $213e4d2df823067d$var$focusFirst(candidates) { + const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement; + for (const candidate of candidates) { + // if focus is already where we want to go, we don't want to keep going through the candidates + if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return; + candidate.focus(); + if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return; + } +} +/** + * Wraps an array around itself at a given start index + * Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']` + */ +function $213e4d2df823067d$var$wrapArray(array, startIndex) { + return array.map((_, index) => array[(startIndex + index) % array.length]); +} +/** + * This is the "meat" of the typeahead matching logic. It takes in all the values, + * the search and the current match, and returns the next match (or `undefined`). + * + * We normalize the search because if a user has repeatedly pressed a character, + * we want the exact same behavior as if we only had that one character + * (ie. cycle through options starting with that character) + * + * We also reorder the values by wrapping the array around the current match. + * This is so we always look forward from the current match, and picking the first + * match will always be the correct one. + * + * Finally, if the normalized search is exactly one character, we exclude the + * current match from the values because otherwise it would be the first to match always + * and focus would never move. This is as opposed to the regular case, where we + * don't want focus to move if the current match still matches. + */ +function $213e4d2df823067d$var$getNextMatch(values, search, currentMatch) { + const isRepeated = search.length > 1 && Array.from(search).every(char => char === search[0]); + const normalizedSearch = isRepeated ? search[0] : search; + const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1; + let wrappedValues = $213e4d2df823067d$var$wrapArray(values, Math.max(currentMatchIndex, 0)); + const excludeCurrentMatch = normalizedSearch.length === 1; + if (excludeCurrentMatch) wrappedValues = wrappedValues.filter(v => v !== currentMatch); + const nextMatch = wrappedValues.find(value => value.toLowerCase().startsWith(normalizedSearch.toLowerCase())); + return nextMatch !== currentMatch ? nextMatch : undefined; +} +// Determine if a point is inside of a polygon. +// Based on https://github.com/substack/point-in-polygon +function $213e4d2df823067d$var$isPointInPolygon(point, polygon) { + const { + x: x, + y: y + } = point; + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x; + const yi = polygon[i].y; + const xj = polygon[j].x; + const yj = polygon[j].y; // prettier-ignore + const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; +} +function $213e4d2df823067d$var$isPointerInGraceArea(event, area) { + if (!area) return false; + const cursorPos = { + x: event.clientX, + y: event.clientY + }; + return $213e4d2df823067d$var$isPointInPolygon(cursorPos, area); +} +function $213e4d2df823067d$var$whenMouse(handler) { + return event => event.pointerType === 'mouse' ? handler(event) : undefined; +} +const $213e4d2df823067d$export$be92b6f5f03c0fe9 = $213e4d2df823067d$export$d9b273488cd8ce6f; +const $213e4d2df823067d$export$b688253958b8dfe7 = $213e4d2df823067d$export$9fa5ebd18bee4d43; +const $213e4d2df823067d$export$602eac185826482c = $213e4d2df823067d$export$793392f970497feb; +const $213e4d2df823067d$export$7c6e2c02157bb7d2 = $213e4d2df823067d$export$479f0f2f71193efe; +const $213e4d2df823067d$export$eb2fcfdbd7ba97d4 = $213e4d2df823067d$export$22a631d1f72787bb; +const $213e4d2df823067d$export$b04be29aa201d4f5 = $213e4d2df823067d$export$dd37bec0e8a99143; +const $213e4d2df823067d$export$6d08773d2e66f8f2 = $213e4d2df823067d$export$2ce376c2cc3355c8; +const $213e4d2df823067d$export$16ce288f89fa631c = $213e4d2df823067d$export$f6f243521332502d; +const $213e4d2df823067d$export$a98f0dcb43a68a25 = $213e4d2df823067d$export$ea2200c9eee416b3; +const $213e4d2df823067d$export$371ab307eab489c0 = $213e4d2df823067d$export$69bd225e9817f6d0; +const $213e4d2df823067d$export$c3468e2714d175fa = $213e4d2df823067d$export$a2593e23056970a3; +const $213e4d2df823067d$export$1ff3c3f08ae963c0 = $213e4d2df823067d$export$1cec7dcdd713e220; +const $213e4d2df823067d$export$21b07c8f274aebd5 = $213e4d2df823067d$export$bcdda4773debf5fa; +const $213e4d2df823067d$export$d7a01e11500dfb6f = $213e4d2df823067d$export$71bdb9d1e2909932; +const $213e4d2df823067d$export$2ea8a7a591ac5eac = $213e4d2df823067d$export$5fbbb3ba7297405f; +const $213e4d2df823067d$export$6d4de93b380beddf = $213e4d2df823067d$export$e7142ab31822bde6; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-popper/dist/index.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-popper/dist/index.js ***! + \******************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $50Iv9$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $50Iv9$react = __webpack_require__(/*! react */ "react"); +var $50Iv9$floatinguireactdom = __webpack_require__(/*! @floating-ui/react-dom */ "../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.esm.js"); +var $50Iv9$radixuireactarrow = __webpack_require__(/*! @radix-ui/react-arrow */ "../../../node_modules/@radix-ui/react-arrow/dist/index.js"); +var $50Iv9$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $50Iv9$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $50Iv9$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $50Iv9$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +var $50Iv9$radixuireactuselayouteffect = __webpack_require__(/*! @radix-ui/react-use-layout-effect */ "../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js"); +var $50Iv9$radixuireactusesize = __webpack_require__(/*! @radix-ui/react-use-size */ "../../../node_modules/@radix-ui/react-use-size/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createPopperScope", () => $34310caa050a8d63$export$722aac194ae923); +$parcel$export(module.exports, "Popper", () => $34310caa050a8d63$export$badac9ada3a0bdf9); +$parcel$export(module.exports, "PopperAnchor", () => $34310caa050a8d63$export$ecd4e1ccab6ed6d); +$parcel$export(module.exports, "PopperContent", () => $34310caa050a8d63$export$bc4ae5855d3c4fc); +$parcel$export(module.exports, "PopperArrow", () => $34310caa050a8d63$export$79d62cd4e10a3fd0); +$parcel$export(module.exports, "Root", () => $34310caa050a8d63$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Anchor", () => $34310caa050a8d63$export$b688253958b8dfe7); +$parcel$export(module.exports, "Content", () => $34310caa050a8d63$export$7c6e2c02157bb7d2); +$parcel$export(module.exports, "Arrow", () => $34310caa050a8d63$export$21b07c8f274aebd5); +$parcel$export(module.exports, "SIDE_OPTIONS", () => $34310caa050a8d63$export$36f0086da09c4b9f); +$parcel$export(module.exports, "ALIGN_OPTIONS", () => $34310caa050a8d63$export$3671ffab7b302fc9); +const $34310caa050a8d63$export$36f0086da09c4b9f = ['top', 'right', 'bottom', 'left']; +const $34310caa050a8d63$export$3671ffab7b302fc9 = ['start', 'center', 'end']; +/* ------------------------------------------------------------------------------------------------- + * Popper + * -----------------------------------------------------------------------------------------------*/ +const $34310caa050a8d63$var$POPPER_NAME = 'Popper'; +const [$34310caa050a8d63$var$createPopperContext, $34310caa050a8d63$export$722aac194ae923] = $50Iv9$radixuireactcontext.createContextScope($34310caa050a8d63$var$POPPER_NAME); +const [$34310caa050a8d63$var$PopperProvider, $34310caa050a8d63$var$usePopperContext] = $34310caa050a8d63$var$createPopperContext($34310caa050a8d63$var$POPPER_NAME); +const $34310caa050a8d63$export$badac9ada3a0bdf9 = props => { + const { + __scopePopper: __scopePopper, + children: children + } = props; + const [anchor, setAnchor] = $50Iv9$react.useState(null); + return /*#__PURE__*/$50Iv9$react.createElement($34310caa050a8d63$var$PopperProvider, { + scope: __scopePopper, + anchor: anchor, + onAnchorChange: setAnchor + }, children); +}; +/*#__PURE__*/ +Object.assign($34310caa050a8d63$export$badac9ada3a0bdf9, { + displayName: $34310caa050a8d63$var$POPPER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * PopperAnchor + * -----------------------------------------------------------------------------------------------*/ +const $34310caa050a8d63$var$ANCHOR_NAME = 'PopperAnchor'; +const $34310caa050a8d63$export$ecd4e1ccab6ed6d = /*#__PURE__*/$50Iv9$react.forwardRef((props, forwardedRef) => { + const { + __scopePopper: __scopePopper, + virtualRef: virtualRef, + ...anchorProps + } = props; + const context = $34310caa050a8d63$var$usePopperContext($34310caa050a8d63$var$ANCHOR_NAME, __scopePopper); + const ref = $50Iv9$react.useRef(null); + const composedRefs = $50Iv9$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + $50Iv9$react.useEffect(() => { + // Consumer can anchor the popper to something that isn't + // a DOM node e.g. pointer position, so we override the + // `anchorRef` with their virtual ref in this case. + context.onAnchorChange((virtualRef === null || virtualRef === void 0 ? void 0 : virtualRef.current) || ref.current); + }); + return virtualRef ? null : /*#__PURE__*/$50Iv9$react.createElement($50Iv9$radixuireactprimitive.Primitive.div, $parcel$interopDefault($50Iv9$babelruntimehelpersextends)({}, anchorProps, { + ref: composedRefs + })); +}); +/*#__PURE__*/ +Object.assign($34310caa050a8d63$export$ecd4e1ccab6ed6d, { + displayName: $34310caa050a8d63$var$ANCHOR_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * PopperContent + * -----------------------------------------------------------------------------------------------*/ +const $34310caa050a8d63$var$CONTENT_NAME = 'PopperContent'; +const [$34310caa050a8d63$var$PopperContentProvider, $34310caa050a8d63$var$useContentContext] = $34310caa050a8d63$var$createPopperContext($34310caa050a8d63$var$CONTENT_NAME); +const $34310caa050a8d63$export$bc4ae5855d3c4fc = /*#__PURE__*/$50Iv9$react.forwardRef((props, forwardedRef) => { + var _arrowSize$width, _arrowSize$height, _middlewareData$arrow, _middlewareData$arrow2, _middlewareData$arrow3, _middlewareData$trans, _middlewareData$trans2, _middlewareData$hide; + const { + __scopePopper: __scopePopper, + side = 'bottom', + sideOffset = 0, + align = 'center', + alignOffset = 0, + arrowPadding = 0, + collisionBoundary = [], + collisionPadding: collisionPaddingProp = 0, + sticky = 'partial', + hideWhenDetached = false, + avoidCollisions = true, + onPlaced: onPlaced, + ...contentProps + } = props; + const context = $34310caa050a8d63$var$usePopperContext($34310caa050a8d63$var$CONTENT_NAME, __scopePopper); + const [content, setContent] = $50Iv9$react.useState(null); + const composedRefs = $50Iv9$radixuireactcomposerefs.useComposedRefs(forwardedRef, node => setContent(node)); + const [arrow, setArrow] = $50Iv9$react.useState(null); + const arrowSize = $50Iv9$radixuireactusesize.useSize(arrow); + const arrowWidth = (_arrowSize$width = arrowSize === null || arrowSize === void 0 ? void 0 : arrowSize.width) !== null && _arrowSize$width !== void 0 ? _arrowSize$width : 0; + const arrowHeight = (_arrowSize$height = arrowSize === null || arrowSize === void 0 ? void 0 : arrowSize.height) !== null && _arrowSize$height !== void 0 ? _arrowSize$height : 0; + const desiredPlacement = side + (align !== 'center' ? '-' + align : ''); + const collisionPadding = typeof collisionPaddingProp === 'number' ? collisionPaddingProp : { + top: 0, + right: 0, + bottom: 0, + left: 0, + ...collisionPaddingProp + }; + const boundary = Array.isArray(collisionBoundary) ? collisionBoundary : [collisionBoundary]; + const hasExplicitBoundaries = boundary.length > 0; + const detectOverflowOptions = { + padding: collisionPadding, + boundary: boundary.filter($34310caa050a8d63$var$isNotNull), + // with `strategy: 'fixed'`, this is the only way to get it to respect boundaries + altBoundary: hasExplicitBoundaries + }; + const { + refs: refs, + floatingStyles: floatingStyles, + placement: placement, + isPositioned: isPositioned, + middlewareData: middlewareData + } = $50Iv9$floatinguireactdom.useFloating({ + // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues + strategy: 'fixed', + placement: desiredPlacement, + whileElementsMounted: $50Iv9$floatinguireactdom.autoUpdate, + elements: { + reference: context.anchor + }, + middleware: [$50Iv9$floatinguireactdom.offset({ + mainAxis: sideOffset + arrowHeight, + alignmentAxis: alignOffset + }), avoidCollisions && $50Iv9$floatinguireactdom.shift({ + mainAxis: true, + crossAxis: false, + limiter: sticky === 'partial' ? $50Iv9$floatinguireactdom.limitShift() : undefined, + ...detectOverflowOptions + }), avoidCollisions && $50Iv9$floatinguireactdom.flip({ + ...detectOverflowOptions + }), $50Iv9$floatinguireactdom.size({ + ...detectOverflowOptions, + apply: _ref => { + let { + elements: elements, + rects: rects, + availableWidth: availableWidth, + availableHeight: availableHeight + } = _ref; + const { + width: anchorWidth, + height: anchorHeight + } = rects.reference; + const contentStyle = elements.floating.style; + contentStyle.setProperty('--radix-popper-available-width', `${availableWidth}px`); + contentStyle.setProperty('--radix-popper-available-height', `${availableHeight}px`); + contentStyle.setProperty('--radix-popper-anchor-width', `${anchorWidth}px`); + contentStyle.setProperty('--radix-popper-anchor-height', `${anchorHeight}px`); + } + }), arrow && $50Iv9$floatinguireactdom.arrow({ + element: arrow, + padding: arrowPadding + }), $34310caa050a8d63$var$transformOrigin({ + arrowWidth: arrowWidth, + arrowHeight: arrowHeight + }), hideWhenDetached && $50Iv9$floatinguireactdom.hide({ + strategy: 'referenceHidden' + })] + }); + const [placedSide, placedAlign] = $34310caa050a8d63$var$getSideAndAlignFromPlacement(placement); + const handlePlaced = $50Iv9$radixuireactusecallbackref.useCallbackRef(onPlaced); + $50Iv9$radixuireactuselayouteffect.useLayoutEffect(() => { + if (isPositioned) handlePlaced === null || handlePlaced === void 0 || handlePlaced(); + }, [isPositioned, handlePlaced]); + const arrowX = (_middlewareData$arrow = middlewareData.arrow) === null || _middlewareData$arrow === void 0 ? void 0 : _middlewareData$arrow.x; + const arrowY = (_middlewareData$arrow2 = middlewareData.arrow) === null || _middlewareData$arrow2 === void 0 ? void 0 : _middlewareData$arrow2.y; + const cannotCenterArrow = ((_middlewareData$arrow3 = middlewareData.arrow) === null || _middlewareData$arrow3 === void 0 ? void 0 : _middlewareData$arrow3.centerOffset) !== 0; + const [contentZIndex, setContentZIndex] = $50Iv9$react.useState(); + $50Iv9$radixuireactuselayouteffect.useLayoutEffect(() => { + if (content) setContentZIndex(window.getComputedStyle(content).zIndex); + }, [content]); + return /*#__PURE__*/$50Iv9$react.createElement("div", { + ref: refs.setFloating, + "data-radix-popper-content-wrapper": "", + style: { + ...floatingStyles, + transform: isPositioned ? floatingStyles.transform : 'translate(0, -200%)', + // keep off the page when measuring + minWidth: 'max-content', + zIndex: contentZIndex, + ['--radix-popper-transform-origin']: [(_middlewareData$trans = middlewareData.transformOrigin) === null || _middlewareData$trans === void 0 ? void 0 : _middlewareData$trans.x, (_middlewareData$trans2 = middlewareData.transformOrigin) === null || _middlewareData$trans2 === void 0 ? void 0 : _middlewareData$trans2.y].join(' ') + } // Floating UI interally calculates logical alignment based the `dir` attribute on + , + + dir: props.dir + }, /*#__PURE__*/$50Iv9$react.createElement($34310caa050a8d63$var$PopperContentProvider, { + scope: __scopePopper, + placedSide: placedSide, + onArrowChange: setArrow, + arrowX: arrowX, + arrowY: arrowY, + shouldHideArrow: cannotCenterArrow + }, /*#__PURE__*/$50Iv9$react.createElement($50Iv9$radixuireactprimitive.Primitive.div, $parcel$interopDefault($50Iv9$babelruntimehelpersextends)({ + "data-side": placedSide, + "data-align": placedAlign + }, contentProps, { + ref: composedRefs, + style: { + ...contentProps.style, + // if the PopperContent hasn't been placed yet (not all measurements done) + // we prevent animations so that users's animation don't kick in too early referring wrong sides + animation: !isPositioned ? 'none' : undefined, + // hide the content if using the hide middleware and should be hidden + opacity: (_middlewareData$hide = middlewareData.hide) !== null && _middlewareData$hide !== void 0 && _middlewareData$hide.referenceHidden ? 0 : undefined + } + })))); +}); +/*#__PURE__*/ +Object.assign($34310caa050a8d63$export$bc4ae5855d3c4fc, { + displayName: $34310caa050a8d63$var$CONTENT_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * PopperArrow + * -----------------------------------------------------------------------------------------------*/ +const $34310caa050a8d63$var$ARROW_NAME = 'PopperArrow'; +const $34310caa050a8d63$var$OPPOSITE_SIDE = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right' +}; +const $34310caa050a8d63$export$79d62cd4e10a3fd0 = /*#__PURE__*/$50Iv9$react.forwardRef(function $34310caa050a8d63$export$79d62cd4e10a3fd0(props, forwardedRef) { + const { + __scopePopper: __scopePopper, + ...arrowProps + } = props; + const contentContext = $34310caa050a8d63$var$useContentContext($34310caa050a8d63$var$ARROW_NAME, __scopePopper); + const baseSide = $34310caa050a8d63$var$OPPOSITE_SIDE[contentContext.placedSide]; + return /*#__PURE__*/ (// we have to use an extra wrapper because `ResizeObserver` (used by `useSize`) + // doesn't report size as we'd expect on SVG elements. + // it reports their bounding box which is effectively the largest path inside the SVG. + $50Iv9$react.createElement("span", { + ref: contentContext.onArrowChange, + style: { + position: 'absolute', + left: contentContext.arrowX, + top: contentContext.arrowY, + [baseSide]: 0, + transformOrigin: { + top: '', + right: '0 0', + bottom: 'center 0', + left: '100% 0' + }[contentContext.placedSide], + transform: { + top: 'translateY(100%)', + right: 'translateY(50%) rotate(90deg) translateX(-50%)', + bottom: `rotate(180deg)`, + left: 'translateY(50%) rotate(-90deg) translateX(50%)' + }[contentContext.placedSide], + visibility: contentContext.shouldHideArrow ? 'hidden' : undefined + } + }, /*#__PURE__*/$50Iv9$react.createElement($50Iv9$radixuireactarrow.Root, $parcel$interopDefault($50Iv9$babelruntimehelpersextends)({}, arrowProps, { + ref: forwardedRef, + style: { + ...arrowProps.style, + // ensures the element can be measured correctly (mostly for if SVG) + display: 'block' + } + }))) + ); +}); +/*#__PURE__*/ +Object.assign($34310caa050a8d63$export$79d62cd4e10a3fd0, { + displayName: $34310caa050a8d63$var$ARROW_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +function $34310caa050a8d63$var$isNotNull(value) { + return value !== null; +} +const $34310caa050a8d63$var$transformOrigin = options => ({ + name: 'transformOrigin', + options: options, + fn(data) { + var _middlewareData$arrow4, _middlewareData$arrow5, _middlewareData$arrow6, _middlewareData$arrow7, _middlewareData$arrow8; + const { + placement: placement, + rects: rects, + middlewareData: middlewareData + } = data; + const cannotCenterArrow = ((_middlewareData$arrow4 = middlewareData.arrow) === null || _middlewareData$arrow4 === void 0 ? void 0 : _middlewareData$arrow4.centerOffset) !== 0; + const isArrowHidden = cannotCenterArrow; + const arrowWidth = isArrowHidden ? 0 : options.arrowWidth; + const arrowHeight = isArrowHidden ? 0 : options.arrowHeight; + const [placedSide, placedAlign] = $34310caa050a8d63$var$getSideAndAlignFromPlacement(placement); + const noArrowAlign = { + start: '0%', + center: '50%', + end: '100%' + }[placedAlign]; + const arrowXCenter = ((_middlewareData$arrow5 = (_middlewareData$arrow6 = middlewareData.arrow) === null || _middlewareData$arrow6 === void 0 ? void 0 : _middlewareData$arrow6.x) !== null && _middlewareData$arrow5 !== void 0 ? _middlewareData$arrow5 : 0) + arrowWidth / 2; + const arrowYCenter = ((_middlewareData$arrow7 = (_middlewareData$arrow8 = middlewareData.arrow) === null || _middlewareData$arrow8 === void 0 ? void 0 : _middlewareData$arrow8.y) !== null && _middlewareData$arrow7 !== void 0 ? _middlewareData$arrow7 : 0) + arrowHeight / 2; + let x = ''; + let y = ''; + if (placedSide === 'bottom') { + x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`; + y = `${-arrowHeight}px`; + } else if (placedSide === 'top') { + x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`; + y = `${rects.floating.height + arrowHeight}px`; + } else if (placedSide === 'right') { + x = `${-arrowHeight}px`; + y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`; + } else if (placedSide === 'left') { + x = `${rects.floating.width + arrowHeight}px`; + y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`; + } + return { + data: { + x: x, + y: y + } + }; + } +}); +function $34310caa050a8d63$var$getSideAndAlignFromPlacement(placement) { + const [side, align = 'center'] = placement.split('-'); + return [side, align]; +} +const $34310caa050a8d63$export$be92b6f5f03c0fe9 = $34310caa050a8d63$export$badac9ada3a0bdf9; +const $34310caa050a8d63$export$b688253958b8dfe7 = $34310caa050a8d63$export$ecd4e1ccab6ed6d; +const $34310caa050a8d63$export$7c6e2c02157bb7d2 = $34310caa050a8d63$export$bc4ae5855d3c4fc; +const $34310caa050a8d63$export$21b07c8f274aebd5 = $34310caa050a8d63$export$79d62cd4e10a3fd0; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-portal/dist/index.js": +/*!******************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-portal/dist/index.js ***! + \******************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $amzHf$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $amzHf$react = __webpack_require__(/*! react */ "react"); +var $amzHf$reactdom = __webpack_require__(/*! react-dom */ "react-dom"); +var $amzHf$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "Portal", () => $913a70b877676c16$export$602eac185826482c); +$parcel$export(module.exports, "Root", () => $913a70b877676c16$export$be92b6f5f03c0fe9); + +/* ------------------------------------------------------------------------------------------------- + * Portal + * -----------------------------------------------------------------------------------------------*/ +const $913a70b877676c16$var$PORTAL_NAME = 'Portal'; +const $913a70b877676c16$export$602eac185826482c = /*#__PURE__*/$amzHf$react.forwardRef((props, forwardedRef) => { + var _globalThis$document; + const { + container = globalThis === null || globalThis === void 0 ? void 0 : (_globalThis$document = globalThis.document) === null || _globalThis$document === void 0 ? void 0 : _globalThis$document.body, + ...portalProps + } = props; + return container ? /*#__PURE__*/$parcel$interopDefault($amzHf$reactdom).createPortal( /*#__PURE__*/$amzHf$react.createElement($amzHf$radixuireactprimitive.Primitive.div, $parcel$interopDefault($amzHf$babelruntimehelpersextends)({}, portalProps, { + ref: forwardedRef + })), container) : null; +}); +/*#__PURE__*/ +Object.assign($913a70b877676c16$export$602eac185826482c, { + displayName: $913a70b877676c16$var$PORTAL_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $913a70b877676c16$export$be92b6f5f03c0fe9 = $913a70b877676c16$export$602eac185826482c; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-presence/dist/index.js": +/*!********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-presence/dist/index.js ***! + \********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $fnLeV$react = __webpack_require__(/*! react */ "react"); +var $fnLeV$reactdom = __webpack_require__(/*! react-dom */ "react-dom"); +var $fnLeV$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $fnLeV$radixuireactuselayouteffect = __webpack_require__(/*! @radix-ui/react-use-layout-effect */ "../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "Presence", () => $a2fa0214bb2735a1$export$99c2b779aa4e8b8b); +function $8f63844556d0d3cd$export$3e6543de14f8614f(initialState, machine) { + return $fnLeV$react.useReducer((state, event) => { + const nextState = machine[state][event]; + return nextState !== null && nextState !== void 0 ? nextState : state; + }, initialState); +} +const $a2fa0214bb2735a1$export$99c2b779aa4e8b8b = props => { + const { + present: present, + children: children + } = props; + const presence = $a2fa0214bb2735a1$var$usePresence(present); + const child = typeof children === 'function' ? children({ + present: presence.isPresent + }) : $fnLeV$react.Children.only(children); + const ref = $fnLeV$radixuireactcomposerefs.useComposedRefs(presence.ref, child.ref); + const forceMount = typeof children === 'function'; + return forceMount || presence.isPresent ? /*#__PURE__*/$fnLeV$react.cloneElement(child, { + ref: ref + }) : null; +}; +$a2fa0214bb2735a1$export$99c2b779aa4e8b8b.displayName = 'Presence'; +/* ------------------------------------------------------------------------------------------------- + * usePresence + * -----------------------------------------------------------------------------------------------*/ +function $a2fa0214bb2735a1$var$usePresence(present) { + const [node1, setNode] = $fnLeV$react.useState(); + const stylesRef = $fnLeV$react.useRef({}); + const prevPresentRef = $fnLeV$react.useRef(present); + const prevAnimationNameRef = $fnLeV$react.useRef('none'); + const initialState = present ? 'mounted' : 'unmounted'; + const [state, send] = $8f63844556d0d3cd$export$3e6543de14f8614f(initialState, { + mounted: { + UNMOUNT: 'unmounted', + ANIMATION_OUT: 'unmountSuspended' + }, + unmountSuspended: { + MOUNT: 'mounted', + ANIMATION_END: 'unmounted' + }, + unmounted: { + MOUNT: 'mounted' + } + }); + $fnLeV$react.useEffect(() => { + const currentAnimationName = $a2fa0214bb2735a1$var$getAnimationName(stylesRef.current); + prevAnimationNameRef.current = state === 'mounted' ? currentAnimationName : 'none'; + }, [state]); + $fnLeV$radixuireactuselayouteffect.useLayoutEffect(() => { + const styles = stylesRef.current; + const wasPresent = prevPresentRef.current; + const hasPresentChanged = wasPresent !== present; + if (hasPresentChanged) { + const prevAnimationName = prevAnimationNameRef.current; + const currentAnimationName = $a2fa0214bb2735a1$var$getAnimationName(styles); + if (present) send('MOUNT');else if (currentAnimationName === 'none' || (styles === null || styles === void 0 ? void 0 : styles.display) === 'none') + // If there is no exit animation or the element is hidden, animations won't run + // so we unmount instantly + send('UNMOUNT');else { + /** + * When `present` changes to `false`, we check changes to animation-name to + * determine whether an animation has started. We chose this approach (reading + * computed styles) because there is no `animationrun` event and `animationstart` + * fires after `animation-delay` has expired which would be too late. + */ + const isAnimating = prevAnimationName !== currentAnimationName; + if (wasPresent && isAnimating) send('ANIMATION_OUT');else send('UNMOUNT'); + } + prevPresentRef.current = present; + } + }, [present, send]); + $fnLeV$radixuireactuselayouteffect.useLayoutEffect(() => { + if (node1) { + /** + * Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel` + * event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we + * make sure we only trigger ANIMATION_END for the currently active animation. + */ + const handleAnimationEnd = event => { + const currentAnimationName = $a2fa0214bb2735a1$var$getAnimationName(stylesRef.current); + const isCurrentAnimation = currentAnimationName.includes(event.animationName); + if (event.target === node1 && isCurrentAnimation) + // With React 18 concurrency this update is applied + // a frame after the animation ends, creating a flash of visible content. + // By manually flushing we ensure they sync within a frame, removing the flash. + $fnLeV$reactdom.flushSync(() => send('ANIMATION_END')); + }; + const handleAnimationStart = event => { + if (event.target === node1) + // if animation occurred, store its name as the previous animation. + prevAnimationNameRef.current = $a2fa0214bb2735a1$var$getAnimationName(stylesRef.current); + }; + node1.addEventListener('animationstart', handleAnimationStart); + node1.addEventListener('animationcancel', handleAnimationEnd); + node1.addEventListener('animationend', handleAnimationEnd); + return () => { + node1.removeEventListener('animationstart', handleAnimationStart); + node1.removeEventListener('animationcancel', handleAnimationEnd); + node1.removeEventListener('animationend', handleAnimationEnd); + }; + } else + // Transition to the unmounted state if the node is removed prematurely. + // We avoid doing so during cleanup as the node may change but still exist. + send('ANIMATION_END'); + }, [node1, send]); + return { + isPresent: ['mounted', 'unmountSuspended'].includes(state), + ref: $fnLeV$react.useCallback(node => { + if (node) stylesRef.current = getComputedStyle(node); + setNode(node); + }, []) + }; +} +/* -----------------------------------------------------------------------------------------------*/ +function $a2fa0214bb2735a1$var$getAnimationName(styles) { + return (styles === null || styles === void 0 ? void 0 : styles.animationName) || 'none'; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-primitive/dist/index.js": +/*!*********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-primitive/dist/index.js ***! + \*********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $iMixA$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $iMixA$react = __webpack_require__(/*! react */ "react"); +var $iMixA$reactdom = __webpack_require__(/*! react-dom */ "react-dom"); +var $iMixA$radixuireactslot = __webpack_require__(/*! @radix-ui/react-slot */ "../../../node_modules/@radix-ui/react-slot/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "Primitive", () => $c3def6332c2749a6$export$250ffa63cdc0d034); +$parcel$export(module.exports, "Root", () => $c3def6332c2749a6$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "dispatchDiscreteCustomEvent", () => $c3def6332c2749a6$export$6d1a0317bde7de7f); +const $c3def6332c2749a6$var$NODES = ['a', 'button', 'div', 'form', 'h2', 'h3', 'img', 'input', 'label', 'li', 'nav', 'ol', 'p', 'span', 'svg', 'ul']; // Temporary while we await merge of this fix: +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/55396 +// prettier-ignore +/* ------------------------------------------------------------------------------------------------- + * Primitive + * -----------------------------------------------------------------------------------------------*/ +const $c3def6332c2749a6$export$250ffa63cdc0d034 = $c3def6332c2749a6$var$NODES.reduce((primitive, node) => { + const Node = /*#__PURE__*/$iMixA$react.forwardRef((props, forwardedRef) => { + const { + asChild: asChild, + ...primitiveProps + } = props; + const Comp = asChild ? $iMixA$radixuireactslot.Slot : node; + $iMixA$react.useEffect(() => { + window[Symbol.for('radix-ui')] = true; + }, []); + return /*#__PURE__*/$iMixA$react.createElement(Comp, $parcel$interopDefault($iMixA$babelruntimehelpersextends)({}, primitiveProps, { + ref: forwardedRef + })); + }); + Node.displayName = `Primitive.${node}`; + return { + ...primitive, + [node]: Node + }; +}, {}); +/* ------------------------------------------------------------------------------------------------- + * Utils + * -----------------------------------------------------------------------------------------------*/ /** + * Flush custom event dispatch + * https://github.com/radix-ui/primitives/pull/1378 + * + * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types. + * + * Internally, React prioritises events in the following order: + * - discrete + * - continuous + * - default + * + * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350 + * + * `discrete` is an important distinction as updates within these events are applied immediately. + * React however, is not able to infer the priority of custom event types due to how they are detected internally. + * Because of this, it's possible for updates from custom events to be unexpectedly batched when + * dispatched by another `discrete` event. + * + * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch. + * This utility should be used when dispatching a custom event from within another `discrete` event, this utility + * is not nessesary when dispatching known event types, or if dispatching a custom type inside a non-discrete event. + * For example: + * + * dispatching a known click 👎 + * target.dispatchEvent(new Event(‘click’)) + * + * dispatching a custom type within a non-discrete event 👎 + * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(‘customType’))} + * + * dispatching a custom type within a `discrete` event 👍 + * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(‘customType’))} + * + * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use + * this utility with them. This is because it's possible for those handlers to be called implicitly during render + * e.g. when focus is within a component as it is unmounted, or when managing focus on mount. + */ +function $c3def6332c2749a6$export$6d1a0317bde7de7f(target, event) { + if (target) $iMixA$reactdom.flushSync(() => target.dispatchEvent(event)); +} +/* -----------------------------------------------------------------------------------------------*/ +const $c3def6332c2749a6$export$be92b6f5f03c0fe9 = $c3def6332c2749a6$export$250ffa63cdc0d034; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-roving-focus/dist/index.js": +/*!************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-roving-focus/dist/index.js ***! + \************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $9QJ9Y$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $9QJ9Y$react = __webpack_require__(/*! react */ "react"); +var $9QJ9Y$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $9QJ9Y$radixuireactcollection = __webpack_require__(/*! @radix-ui/react-collection */ "../../../node_modules/@radix-ui/react-collection/dist/index.js"); +var $9QJ9Y$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $9QJ9Y$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $9QJ9Y$radixuireactid = __webpack_require__(/*! @radix-ui/react-id */ "../../../node_modules/@radix-ui/react-id/dist/index.js"); +var $9QJ9Y$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $9QJ9Y$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +var $9QJ9Y$radixuireactusecontrollablestate = __webpack_require__(/*! @radix-ui/react-use-controllable-state */ "../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js"); +var $9QJ9Y$radixuireactdirection = __webpack_require__(/*! @radix-ui/react-direction */ "../../../node_modules/@radix-ui/react-direction/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createRovingFocusGroupScope", () => $0063afae63b3fa70$export$c7109489551a4f4); +$parcel$export(module.exports, "RovingFocusGroup", () => $0063afae63b3fa70$export$8699f7c8af148338); +$parcel$export(module.exports, "RovingFocusGroupItem", () => $0063afae63b3fa70$export$ab9df7c53fe8454); +$parcel$export(module.exports, "Root", () => $0063afae63b3fa70$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Item", () => $0063afae63b3fa70$export$6d08773d2e66f8f2); +const $0063afae63b3fa70$var$ENTRY_FOCUS = 'rovingFocusGroup.onEntryFocus'; +const $0063afae63b3fa70$var$EVENT_OPTIONS = { + bubbles: false, + cancelable: true +}; +/* ------------------------------------------------------------------------------------------------- + * RovingFocusGroup + * -----------------------------------------------------------------------------------------------*/ +const $0063afae63b3fa70$var$GROUP_NAME = 'RovingFocusGroup'; +const [$0063afae63b3fa70$var$Collection, $0063afae63b3fa70$var$useCollection, $0063afae63b3fa70$var$createCollectionScope] = $9QJ9Y$radixuireactcollection.createCollection($0063afae63b3fa70$var$GROUP_NAME); +const [$0063afae63b3fa70$var$createRovingFocusGroupContext, $0063afae63b3fa70$export$c7109489551a4f4] = $9QJ9Y$radixuireactcontext.createContextScope($0063afae63b3fa70$var$GROUP_NAME, [$0063afae63b3fa70$var$createCollectionScope]); +const [$0063afae63b3fa70$var$RovingFocusProvider, $0063afae63b3fa70$var$useRovingFocusContext] = $0063afae63b3fa70$var$createRovingFocusGroupContext($0063afae63b3fa70$var$GROUP_NAME); +const $0063afae63b3fa70$export$8699f7c8af148338 = /*#__PURE__*/$9QJ9Y$react.forwardRef((props, forwardedRef) => { + return /*#__PURE__*/$9QJ9Y$react.createElement($0063afae63b3fa70$var$Collection.Provider, { + scope: props.__scopeRovingFocusGroup + }, /*#__PURE__*/$9QJ9Y$react.createElement($0063afae63b3fa70$var$Collection.Slot, { + scope: props.__scopeRovingFocusGroup + }, /*#__PURE__*/$9QJ9Y$react.createElement($0063afae63b3fa70$var$RovingFocusGroupImpl, $parcel$interopDefault($9QJ9Y$babelruntimehelpersextends)({}, props, { + ref: forwardedRef + })))); +}); +/*#__PURE__*/ +Object.assign($0063afae63b3fa70$export$8699f7c8af148338, { + displayName: $0063afae63b3fa70$var$GROUP_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $0063afae63b3fa70$var$RovingFocusGroupImpl = /*#__PURE__*/$9QJ9Y$react.forwardRef((props, forwardedRef) => { + const { + __scopeRovingFocusGroup: __scopeRovingFocusGroup, + orientation: orientation, + loop = false, + dir: dir, + currentTabStopId: currentTabStopIdProp, + defaultCurrentTabStopId: defaultCurrentTabStopId, + onCurrentTabStopIdChange: onCurrentTabStopIdChange, + onEntryFocus: onEntryFocus, + ...groupProps + } = props; + const ref = $9QJ9Y$react.useRef(null); + const composedRefs = $9QJ9Y$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + const direction = $9QJ9Y$radixuireactdirection.useDirection(dir); + const [currentTabStopId = null, setCurrentTabStopId] = $9QJ9Y$radixuireactusecontrollablestate.useControllableState({ + prop: currentTabStopIdProp, + defaultProp: defaultCurrentTabStopId, + onChange: onCurrentTabStopIdChange + }); + const [isTabbingBackOut, setIsTabbingBackOut] = $9QJ9Y$react.useState(false); + const handleEntryFocus = $9QJ9Y$radixuireactusecallbackref.useCallbackRef(onEntryFocus); + const getItems = $0063afae63b3fa70$var$useCollection(__scopeRovingFocusGroup); + const isClickFocusRef = $9QJ9Y$react.useRef(false); + const [focusableItemsCount, setFocusableItemsCount] = $9QJ9Y$react.useState(0); + $9QJ9Y$react.useEffect(() => { + const node = ref.current; + if (node) { + node.addEventListener($0063afae63b3fa70$var$ENTRY_FOCUS, handleEntryFocus); + return () => node.removeEventListener($0063afae63b3fa70$var$ENTRY_FOCUS, handleEntryFocus); + } + }, [handleEntryFocus]); + return /*#__PURE__*/$9QJ9Y$react.createElement($0063afae63b3fa70$var$RovingFocusProvider, { + scope: __scopeRovingFocusGroup, + orientation: orientation, + dir: direction, + loop: loop, + currentTabStopId: currentTabStopId, + onItemFocus: $9QJ9Y$react.useCallback(tabStopId => setCurrentTabStopId(tabStopId), [setCurrentTabStopId]), + onItemShiftTab: $9QJ9Y$react.useCallback(() => setIsTabbingBackOut(true), []), + onFocusableItemAdd: $9QJ9Y$react.useCallback(() => setFocusableItemsCount(prevCount => prevCount + 1), []), + onFocusableItemRemove: $9QJ9Y$react.useCallback(() => setFocusableItemsCount(prevCount => prevCount - 1), []) + }, /*#__PURE__*/$9QJ9Y$react.createElement($9QJ9Y$radixuireactprimitive.Primitive.div, $parcel$interopDefault($9QJ9Y$babelruntimehelpersextends)({ + tabIndex: isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0, + "data-orientation": orientation + }, groupProps, { + ref: composedRefs, + style: { + outline: 'none', + ...props.style + }, + onMouseDown: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onMouseDown, () => { + isClickFocusRef.current = true; + }), + onFocus: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onFocus, event => { + // We normally wouldn't need this check, because we already check + // that the focus is on the current target and not bubbling to it. + // We do this because Safari doesn't focus buttons when clicked, and + // instead, the wrapper will get focused and not through a bubbling event. + const isKeyboardFocus = !isClickFocusRef.current; + if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) { + const entryFocusEvent = new CustomEvent($0063afae63b3fa70$var$ENTRY_FOCUS, $0063afae63b3fa70$var$EVENT_OPTIONS); + event.currentTarget.dispatchEvent(entryFocusEvent); + if (!entryFocusEvent.defaultPrevented) { + const items = getItems().filter(item => item.focusable); + const activeItem = items.find(item => item.active); + const currentItem = items.find(item => item.id === currentTabStopId); + const candidateItems = [activeItem, currentItem, ...items].filter(Boolean); + const candidateNodes = candidateItems.map(item => item.ref.current); + $0063afae63b3fa70$var$focusFirst(candidateNodes); + } + } + isClickFocusRef.current = false; + }), + onBlur: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onBlur, () => setIsTabbingBackOut(false)) + }))); +}); +/* ------------------------------------------------------------------------------------------------- + * RovingFocusGroupItem + * -----------------------------------------------------------------------------------------------*/ +const $0063afae63b3fa70$var$ITEM_NAME = 'RovingFocusGroupItem'; +const $0063afae63b3fa70$export$ab9df7c53fe8454 = /*#__PURE__*/$9QJ9Y$react.forwardRef((props, forwardedRef) => { + const { + __scopeRovingFocusGroup: __scopeRovingFocusGroup, + focusable = true, + active = false, + tabStopId: tabStopId, + ...itemProps + } = props; + const autoId = $9QJ9Y$radixuireactid.useId(); + const id = tabStopId || autoId; + const context = $0063afae63b3fa70$var$useRovingFocusContext($0063afae63b3fa70$var$ITEM_NAME, __scopeRovingFocusGroup); + const isCurrentTabStop = context.currentTabStopId === id; + const getItems = $0063afae63b3fa70$var$useCollection(__scopeRovingFocusGroup); + const { + onFocusableItemAdd: onFocusableItemAdd, + onFocusableItemRemove: onFocusableItemRemove + } = context; + $9QJ9Y$react.useEffect(() => { + if (focusable) { + onFocusableItemAdd(); + return () => onFocusableItemRemove(); + } + }, [focusable, onFocusableItemAdd, onFocusableItemRemove]); + return /*#__PURE__*/$9QJ9Y$react.createElement($0063afae63b3fa70$var$Collection.ItemSlot, { + scope: __scopeRovingFocusGroup, + id: id, + focusable: focusable, + active: active + }, /*#__PURE__*/$9QJ9Y$react.createElement($9QJ9Y$radixuireactprimitive.Primitive.span, $parcel$interopDefault($9QJ9Y$babelruntimehelpersextends)({ + tabIndex: isCurrentTabStop ? 0 : -1, + "data-orientation": context.orientation + }, itemProps, { + ref: forwardedRef, + onMouseDown: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onMouseDown, event => { + // We prevent focusing non-focusable items on `mousedown`. + // Even though the item has tabIndex={-1}, that only means take it out of the tab order. + if (!focusable) event.preventDefault(); // Safari doesn't focus a button when clicked so we run our logic on mousedown also + else context.onItemFocus(id); + }), + onFocus: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onFocus, () => context.onItemFocus(id)), + onKeyDown: $9QJ9Y$radixuiprimitive.composeEventHandlers(props.onKeyDown, event => { + if (event.key === 'Tab' && event.shiftKey) { + context.onItemShiftTab(); + return; + } + if (event.target !== event.currentTarget) return; + const focusIntent = $0063afae63b3fa70$var$getFocusIntent(event, context.orientation, context.dir); + if (focusIntent !== undefined) { + event.preventDefault(); + const items = getItems().filter(item => item.focusable); + let candidateNodes = items.map(item => item.ref.current); + if (focusIntent === 'last') candidateNodes.reverse();else if (focusIntent === 'prev' || focusIntent === 'next') { + if (focusIntent === 'prev') candidateNodes.reverse(); + const currentIndex = candidateNodes.indexOf(event.currentTarget); + candidateNodes = context.loop ? $0063afae63b3fa70$var$wrapArray(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1); + } + /** + * Imperative focus during keydown is risky so we prevent React's batching updates + * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332 + */ + setTimeout(() => $0063afae63b3fa70$var$focusFirst(candidateNodes)); + } + }) + }))); +}); +/*#__PURE__*/ +Object.assign($0063afae63b3fa70$export$ab9df7c53fe8454, { + displayName: $0063afae63b3fa70$var$ITEM_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ // prettier-ignore +const $0063afae63b3fa70$var$MAP_KEY_TO_FOCUS_INTENT = { + ArrowLeft: 'prev', + ArrowUp: 'prev', + ArrowRight: 'next', + ArrowDown: 'next', + PageUp: 'first', + Home: 'first', + PageDown: 'last', + End: 'last' +}; +function $0063afae63b3fa70$var$getDirectionAwareKey(key, dir) { + if (dir !== 'rtl') return key; + return key === 'ArrowLeft' ? 'ArrowRight' : key === 'ArrowRight' ? 'ArrowLeft' : key; +} +function $0063afae63b3fa70$var$getFocusIntent(event, orientation, dir) { + const key = $0063afae63b3fa70$var$getDirectionAwareKey(event.key, dir); + if (orientation === 'vertical' && ['ArrowLeft', 'ArrowRight'].includes(key)) return undefined; + if (orientation === 'horizontal' && ['ArrowUp', 'ArrowDown'].includes(key)) return undefined; + return $0063afae63b3fa70$var$MAP_KEY_TO_FOCUS_INTENT[key]; +} +function $0063afae63b3fa70$var$focusFirst(candidates) { + const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement; + for (const candidate of candidates) { + // if focus is already where we want to go, we don't want to keep going through the candidates + if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return; + candidate.focus(); + if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return; + } +} +/** + * Wraps an array around itself at a given start index + * Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']` + */ +function $0063afae63b3fa70$var$wrapArray(array, startIndex) { + return array.map((_, index) => array[(startIndex + index) % array.length]); +} +const $0063afae63b3fa70$export$be92b6f5f03c0fe9 = $0063afae63b3fa70$export$8699f7c8af148338; +const $0063afae63b3fa70$export$6d08773d2e66f8f2 = $0063afae63b3fa70$export$ab9df7c53fe8454; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-slot/dist/index.js": +/*!****************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-slot/dist/index.js ***! + \****************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $dAvBt$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $dAvBt$react = __webpack_require__(/*! react */ "react"); +var $dAvBt$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "Slot", () => $82dc8d030dec7549$export$8c6ed5c666ac1360); +$parcel$export(module.exports, "Slottable", () => $82dc8d030dec7549$export$d9f1ccf0bdb05d45); +$parcel$export(module.exports, "Root", () => $82dc8d030dec7549$export$be92b6f5f03c0fe9); + +/* ------------------------------------------------------------------------------------------------- + * Slot + * -----------------------------------------------------------------------------------------------*/ +const $82dc8d030dec7549$export$8c6ed5c666ac1360 = /*#__PURE__*/$dAvBt$react.forwardRef((props, forwardedRef) => { + const { + children: children, + ...slotProps + } = props; + const childrenArray = $dAvBt$react.Children.toArray(children); + const slottable = childrenArray.find($82dc8d030dec7549$var$isSlottable); + if (slottable) { + // the new element to render is the one passed as a child of `Slottable` + const newElement = slottable.props.children; + const newChildren = childrenArray.map(child => { + if (child === slottable) { + // because the new element will be the one rendered, we are only interested + // in grabbing its children (`newElement.props.children`) + if ($dAvBt$react.Children.count(newElement) > 1) return $dAvBt$react.Children.only(null); + return /*#__PURE__*/$dAvBt$react.isValidElement(newElement) ? newElement.props.children : null; + } else return child; + }); + return /*#__PURE__*/$dAvBt$react.createElement($82dc8d030dec7549$var$SlotClone, $parcel$interopDefault($dAvBt$babelruntimehelpersextends)({}, slotProps, { + ref: forwardedRef + }), /*#__PURE__*/$dAvBt$react.isValidElement(newElement) ? /*#__PURE__*/$dAvBt$react.cloneElement(newElement, undefined, newChildren) : null); + } + return /*#__PURE__*/$dAvBt$react.createElement($82dc8d030dec7549$var$SlotClone, $parcel$interopDefault($dAvBt$babelruntimehelpersextends)({}, slotProps, { + ref: forwardedRef + }), children); +}); +$82dc8d030dec7549$export$8c6ed5c666ac1360.displayName = 'Slot'; +/* ------------------------------------------------------------------------------------------------- + * SlotClone + * -----------------------------------------------------------------------------------------------*/ +const $82dc8d030dec7549$var$SlotClone = /*#__PURE__*/$dAvBt$react.forwardRef((props, forwardedRef) => { + const { + children: children, + ...slotProps + } = props; + if ( /*#__PURE__*/$dAvBt$react.isValidElement(children)) return /*#__PURE__*/$dAvBt$react.cloneElement(children, { + ...$82dc8d030dec7549$var$mergeProps(slotProps, children.props), + ref: forwardedRef ? $dAvBt$radixuireactcomposerefs.composeRefs(forwardedRef, children.ref) : children.ref + }); + return $dAvBt$react.Children.count(children) > 1 ? $dAvBt$react.Children.only(null) : null; +}); +$82dc8d030dec7549$var$SlotClone.displayName = 'SlotClone'; +/* ------------------------------------------------------------------------------------------------- + * Slottable + * -----------------------------------------------------------------------------------------------*/ +const $82dc8d030dec7549$export$d9f1ccf0bdb05d45 = _ref => { + let { + children: children + } = _ref; + return /*#__PURE__*/$dAvBt$react.createElement($dAvBt$react.Fragment, null, children); +}; +/* ---------------------------------------------------------------------------------------------- */ +function $82dc8d030dec7549$var$isSlottable(child) { + return /*#__PURE__*/$dAvBt$react.isValidElement(child) && child.type === $82dc8d030dec7549$export$d9f1ccf0bdb05d45; +} +function $82dc8d030dec7549$var$mergeProps(slotProps, childProps) { + // all child props should override + const overrideProps = { + ...childProps + }; + for (const propName in childProps) { + const slotPropValue = slotProps[propName]; + const childPropValue = childProps[propName]; + const isHandler = /^on[A-Z]/.test(propName); + if (isHandler) { + // if the handler exists on both, we compose them + if (slotPropValue && childPropValue) overrideProps[propName] = function () { + childPropValue(...arguments); + slotPropValue(...arguments); + };else if (slotPropValue) overrideProps[propName] = slotPropValue; + } else if (propName === 'style') overrideProps[propName] = { + ...slotPropValue, + ...childPropValue + };else if (propName === 'className') overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' '); + } + return { + ...slotProps, + ...overrideProps + }; +} +const $82dc8d030dec7549$export$be92b6f5f03c0fe9 = $82dc8d030dec7549$export$8c6ed5c666ac1360; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-tooltip/dist/index.js": +/*!*******************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-tooltip/dist/index.js ***! + \*******************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $iVrL9$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $iVrL9$react = __webpack_require__(/*! react */ "react"); +var $iVrL9$radixuiprimitive = __webpack_require__(/*! @radix-ui/primitive */ "../../../node_modules/@radix-ui/primitive/dist/index.js"); +var $iVrL9$radixuireactcomposerefs = __webpack_require__(/*! @radix-ui/react-compose-refs */ "../../../node_modules/@radix-ui/react-compose-refs/dist/index.js"); +var $iVrL9$radixuireactcontext = __webpack_require__(/*! @radix-ui/react-context */ "../../../node_modules/@radix-ui/react-context/dist/index.js"); +var $iVrL9$radixuireactdismissablelayer = __webpack_require__(/*! @radix-ui/react-dismissable-layer */ "../../../node_modules/@radix-ui/react-dismissable-layer/dist/index.js"); +var $iVrL9$radixuireactid = __webpack_require__(/*! @radix-ui/react-id */ "../../../node_modules/@radix-ui/react-id/dist/index.js"); +var $iVrL9$radixuireactpopper = __webpack_require__(/*! @radix-ui/react-popper */ "../../../node_modules/@radix-ui/react-popper/dist/index.js"); +var $iVrL9$radixuireactportal = __webpack_require__(/*! @radix-ui/react-portal */ "../../../node_modules/@radix-ui/react-portal/dist/index.js"); +var $iVrL9$radixuireactpresence = __webpack_require__(/*! @radix-ui/react-presence */ "../../../node_modules/@radix-ui/react-presence/dist/index.js"); +var $iVrL9$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +var $iVrL9$radixuireactslot = __webpack_require__(/*! @radix-ui/react-slot */ "../../../node_modules/@radix-ui/react-slot/dist/index.js"); +var $iVrL9$radixuireactusecontrollablestate = __webpack_require__(/*! @radix-ui/react-use-controllable-state */ "../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js"); +var $iVrL9$radixuireactvisuallyhidden = __webpack_require__(/*! @radix-ui/react-visually-hidden */ "../../../node_modules/@radix-ui/react-visually-hidden/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "createTooltipScope", () => $c34afbc43c90cc6f$export$1c540a2224f0d865); +$parcel$export(module.exports, "TooltipProvider", () => $c34afbc43c90cc6f$export$f78649fb9ca566b8); +$parcel$export(module.exports, "Tooltip", () => $c34afbc43c90cc6f$export$28c660c63b792dea); +$parcel$export(module.exports, "TooltipTrigger", () => $c34afbc43c90cc6f$export$8c610744efcf8a1d); +$parcel$export(module.exports, "TooltipPortal", () => $c34afbc43c90cc6f$export$7b36b8f925ab7497); +$parcel$export(module.exports, "TooltipContent", () => $c34afbc43c90cc6f$export$e9003e2be37ec060); +$parcel$export(module.exports, "TooltipArrow", () => $c34afbc43c90cc6f$export$c27ee0ad710f7559); +$parcel$export(module.exports, "Provider", () => $c34afbc43c90cc6f$export$2881499e37b75b9a); +$parcel$export(module.exports, "Root", () => $c34afbc43c90cc6f$export$be92b6f5f03c0fe9); +$parcel$export(module.exports, "Trigger", () => $c34afbc43c90cc6f$export$41fb9f06171c75f4); +$parcel$export(module.exports, "Portal", () => $c34afbc43c90cc6f$export$602eac185826482c); +$parcel$export(module.exports, "Content", () => $c34afbc43c90cc6f$export$7c6e2c02157bb7d2); +$parcel$export(module.exports, "Arrow", () => $c34afbc43c90cc6f$export$21b07c8f274aebd5); +const [$c34afbc43c90cc6f$var$createTooltipContext, $c34afbc43c90cc6f$export$1c540a2224f0d865] = $iVrL9$radixuireactcontext.createContextScope('Tooltip', [$iVrL9$radixuireactpopper.createPopperScope]); +const $c34afbc43c90cc6f$var$usePopperScope = $iVrL9$radixuireactpopper.createPopperScope(); +/* ------------------------------------------------------------------------------------------------- + * TooltipProvider + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$PROVIDER_NAME = 'TooltipProvider'; +const $c34afbc43c90cc6f$var$DEFAULT_DELAY_DURATION = 700; +const $c34afbc43c90cc6f$var$TOOLTIP_OPEN = 'tooltip.open'; +const [$c34afbc43c90cc6f$var$TooltipProviderContextProvider, $c34afbc43c90cc6f$var$useTooltipProviderContext] = $c34afbc43c90cc6f$var$createTooltipContext($c34afbc43c90cc6f$var$PROVIDER_NAME); +const $c34afbc43c90cc6f$export$f78649fb9ca566b8 = props => { + const { + __scopeTooltip: __scopeTooltip, + delayDuration = $c34afbc43c90cc6f$var$DEFAULT_DELAY_DURATION, + skipDelayDuration = 300, + disableHoverableContent = false, + children: children + } = props; + const [isOpenDelayed, setIsOpenDelayed] = $iVrL9$react.useState(true); + const isPointerInTransitRef = $iVrL9$react.useRef(false); + const skipDelayTimerRef = $iVrL9$react.useRef(0); + $iVrL9$react.useEffect(() => { + const skipDelayTimer = skipDelayTimerRef.current; + return () => window.clearTimeout(skipDelayTimer); + }, []); + return /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$TooltipProviderContextProvider, { + scope: __scopeTooltip, + isOpenDelayed: isOpenDelayed, + delayDuration: delayDuration, + onOpen: $iVrL9$react.useCallback(() => { + window.clearTimeout(skipDelayTimerRef.current); + setIsOpenDelayed(false); + }, []), + onClose: $iVrL9$react.useCallback(() => { + window.clearTimeout(skipDelayTimerRef.current); + skipDelayTimerRef.current = window.setTimeout(() => setIsOpenDelayed(true), skipDelayDuration); + }, [skipDelayDuration]), + isPointerInTransitRef: isPointerInTransitRef, + onPointerInTransitChange: $iVrL9$react.useCallback(inTransit => { + isPointerInTransitRef.current = inTransit; + }, []), + disableHoverableContent: disableHoverableContent + }, children); +}; +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$f78649fb9ca566b8, { + displayName: $c34afbc43c90cc6f$var$PROVIDER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * Tooltip + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$TOOLTIP_NAME = 'Tooltip'; +const [$c34afbc43c90cc6f$var$TooltipContextProvider, $c34afbc43c90cc6f$var$useTooltipContext] = $c34afbc43c90cc6f$var$createTooltipContext($c34afbc43c90cc6f$var$TOOLTIP_NAME); +const $c34afbc43c90cc6f$export$28c660c63b792dea = props => { + const { + __scopeTooltip: __scopeTooltip, + children: children, + open: openProp, + defaultOpen = false, + onOpenChange: onOpenChange, + disableHoverableContent: disableHoverableContentProp, + delayDuration: delayDurationProp + } = props; + const providerContext = $c34afbc43c90cc6f$var$useTooltipProviderContext($c34afbc43c90cc6f$var$TOOLTIP_NAME, props.__scopeTooltip); + const popperScope = $c34afbc43c90cc6f$var$usePopperScope(__scopeTooltip); + const [trigger, setTrigger] = $iVrL9$react.useState(null); + const contentId = $iVrL9$radixuireactid.useId(); + const openTimerRef = $iVrL9$react.useRef(0); + const disableHoverableContent = disableHoverableContentProp !== null && disableHoverableContentProp !== void 0 ? disableHoverableContentProp : providerContext.disableHoverableContent; + const delayDuration = delayDurationProp !== null && delayDurationProp !== void 0 ? delayDurationProp : providerContext.delayDuration; + const wasOpenDelayedRef = $iVrL9$react.useRef(false); + const [open1 = false, setOpen] = $iVrL9$radixuireactusecontrollablestate.useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: open => { + if (open) { + providerContext.onOpen(); // as `onChange` is called within a lifecycle method we + // avoid dispatching via `dispatchDiscreteCustomEvent`. + document.dispatchEvent(new CustomEvent($c34afbc43c90cc6f$var$TOOLTIP_OPEN)); + } else providerContext.onClose(); + onOpenChange === null || onOpenChange === void 0 || onOpenChange(open); + } + }); + const stateAttribute = $iVrL9$react.useMemo(() => { + return open1 ? wasOpenDelayedRef.current ? 'delayed-open' : 'instant-open' : 'closed'; + }, [open1]); + const handleOpen = $iVrL9$react.useCallback(() => { + window.clearTimeout(openTimerRef.current); + wasOpenDelayedRef.current = false; + setOpen(true); + }, [setOpen]); + const handleClose = $iVrL9$react.useCallback(() => { + window.clearTimeout(openTimerRef.current); + setOpen(false); + }, [setOpen]); + const handleDelayedOpen = $iVrL9$react.useCallback(() => { + window.clearTimeout(openTimerRef.current); + openTimerRef.current = window.setTimeout(() => { + wasOpenDelayedRef.current = true; + setOpen(true); + }, delayDuration); + }, [delayDuration, setOpen]); + $iVrL9$react.useEffect(() => { + return () => window.clearTimeout(openTimerRef.current); + }, []); + return /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpopper.Root, popperScope, /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$TooltipContextProvider, { + scope: __scopeTooltip, + contentId: contentId, + open: open1, + stateAttribute: stateAttribute, + trigger: trigger, + onTriggerChange: setTrigger, + onTriggerEnter: $iVrL9$react.useCallback(() => { + if (providerContext.isOpenDelayed) handleDelayedOpen();else handleOpen(); + }, [providerContext.isOpenDelayed, handleDelayedOpen, handleOpen]), + onTriggerLeave: $iVrL9$react.useCallback(() => { + if (disableHoverableContent) handleClose();else + // Clear the timer in case the pointer leaves the trigger before the tooltip is opened. + window.clearTimeout(openTimerRef.current); + }, [handleClose, disableHoverableContent]), + onOpen: handleOpen, + onClose: handleClose, + disableHoverableContent: disableHoverableContent + }, children)); +}; +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$28c660c63b792dea, { + displayName: $c34afbc43c90cc6f$var$TOOLTIP_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * TooltipTrigger + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$TRIGGER_NAME = 'TooltipTrigger'; +const $c34afbc43c90cc6f$export$8c610744efcf8a1d = /*#__PURE__*/$iVrL9$react.forwardRef((props, forwardedRef) => { + const { + __scopeTooltip: __scopeTooltip, + ...triggerProps + } = props; + const context = $c34afbc43c90cc6f$var$useTooltipContext($c34afbc43c90cc6f$var$TRIGGER_NAME, __scopeTooltip); + const providerContext = $c34afbc43c90cc6f$var$useTooltipProviderContext($c34afbc43c90cc6f$var$TRIGGER_NAME, __scopeTooltip); + const popperScope = $c34afbc43c90cc6f$var$usePopperScope(__scopeTooltip); + const ref = $iVrL9$react.useRef(null); + const composedRefs = $iVrL9$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref, context.onTriggerChange); + const isPointerDownRef = $iVrL9$react.useRef(false); + const hasPointerMoveOpenedRef = $iVrL9$react.useRef(false); + const handlePointerUp = $iVrL9$react.useCallback(() => isPointerDownRef.current = false, []); + $iVrL9$react.useEffect(() => { + return () => document.removeEventListener('pointerup', handlePointerUp); + }, [handlePointerUp]); + return /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpopper.Anchor, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({ + asChild: true + }, popperScope), /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactprimitive.Primitive.button, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({ + // We purposefully avoid adding `type=button` here because tooltip triggers are also + // commonly anchors and the anchor `type` attribute signifies MIME type. + "aria-describedby": context.open ? context.contentId : undefined, + "data-state": context.stateAttribute + }, triggerProps, { + ref: composedRefs, + onPointerMove: $iVrL9$radixuiprimitive.composeEventHandlers(props.onPointerMove, event => { + if (event.pointerType === 'touch') return; + if (!hasPointerMoveOpenedRef.current && !providerContext.isPointerInTransitRef.current) { + context.onTriggerEnter(); + hasPointerMoveOpenedRef.current = true; + } + }), + onPointerLeave: $iVrL9$radixuiprimitive.composeEventHandlers(props.onPointerLeave, () => { + context.onTriggerLeave(); + hasPointerMoveOpenedRef.current = false; + }), + onPointerDown: $iVrL9$radixuiprimitive.composeEventHandlers(props.onPointerDown, () => { + isPointerDownRef.current = true; + document.addEventListener('pointerup', handlePointerUp, { + once: true + }); + }), + onFocus: $iVrL9$radixuiprimitive.composeEventHandlers(props.onFocus, () => { + if (!isPointerDownRef.current) context.onOpen(); + }), + onBlur: $iVrL9$radixuiprimitive.composeEventHandlers(props.onBlur, context.onClose), + onClick: $iVrL9$radixuiprimitive.composeEventHandlers(props.onClick, context.onClose) + }))); +}); +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$8c610744efcf8a1d, { + displayName: $c34afbc43c90cc6f$var$TRIGGER_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * TooltipPortal + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$PORTAL_NAME = 'TooltipPortal'; +const [$c34afbc43c90cc6f$var$PortalProvider, $c34afbc43c90cc6f$var$usePortalContext] = $c34afbc43c90cc6f$var$createTooltipContext($c34afbc43c90cc6f$var$PORTAL_NAME, { + forceMount: undefined +}); +const $c34afbc43c90cc6f$export$7b36b8f925ab7497 = props => { + const { + __scopeTooltip: __scopeTooltip, + forceMount: forceMount, + children: children, + container: container + } = props; + const context = $c34afbc43c90cc6f$var$useTooltipContext($c34afbc43c90cc6f$var$PORTAL_NAME, __scopeTooltip); + return /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$PortalProvider, { + scope: __scopeTooltip, + forceMount: forceMount + }, /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpresence.Presence, { + present: forceMount || context.open + }, /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactportal.Portal, { + asChild: true, + container: container + }, children))); +}; +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$7b36b8f925ab7497, { + displayName: $c34afbc43c90cc6f$var$PORTAL_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * TooltipContent + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$CONTENT_NAME = 'TooltipContent'; +const $c34afbc43c90cc6f$export$e9003e2be37ec060 = /*#__PURE__*/$iVrL9$react.forwardRef((props, forwardedRef) => { + const portalContext = $c34afbc43c90cc6f$var$usePortalContext($c34afbc43c90cc6f$var$CONTENT_NAME, props.__scopeTooltip); + const { + forceMount = portalContext.forceMount, + side = 'top', + ...contentProps + } = props; + const context = $c34afbc43c90cc6f$var$useTooltipContext($c34afbc43c90cc6f$var$CONTENT_NAME, props.__scopeTooltip); + return /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpresence.Presence, { + present: forceMount || context.open + }, context.disableHoverableContent ? /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$TooltipContentImpl, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({ + side: side + }, contentProps, { + ref: forwardedRef + })) : /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$TooltipContentHoverable, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({ + side: side + }, contentProps, { + ref: forwardedRef + }))); +}); +const $c34afbc43c90cc6f$var$TooltipContentHoverable = /*#__PURE__*/$iVrL9$react.forwardRef((props, forwardedRef) => { + const context = $c34afbc43c90cc6f$var$useTooltipContext($c34afbc43c90cc6f$var$CONTENT_NAME, props.__scopeTooltip); + const providerContext = $c34afbc43c90cc6f$var$useTooltipProviderContext($c34afbc43c90cc6f$var$CONTENT_NAME, props.__scopeTooltip); + const ref = $iVrL9$react.useRef(null); + const composedRefs = $iVrL9$radixuireactcomposerefs.useComposedRefs(forwardedRef, ref); + const [pointerGraceArea, setPointerGraceArea] = $iVrL9$react.useState(null); + const { + trigger: trigger, + onClose: onClose + } = context; + const content = ref.current; + const { + onPointerInTransitChange: onPointerInTransitChange + } = providerContext; + const handleRemoveGraceArea = $iVrL9$react.useCallback(() => { + setPointerGraceArea(null); + onPointerInTransitChange(false); + }, [onPointerInTransitChange]); + const handleCreateGraceArea = $iVrL9$react.useCallback((event, hoverTarget) => { + const currentTarget = event.currentTarget; + const exitPoint = { + x: event.clientX, + y: event.clientY + }; + const exitSide = $c34afbc43c90cc6f$var$getExitSideFromRect(exitPoint, currentTarget.getBoundingClientRect()); + const paddedExitPoints = $c34afbc43c90cc6f$var$getPaddedExitPoints(exitPoint, exitSide); + const hoverTargetPoints = $c34afbc43c90cc6f$var$getPointsFromRect(hoverTarget.getBoundingClientRect()); + const graceArea = $c34afbc43c90cc6f$var$getHull([...paddedExitPoints, ...hoverTargetPoints]); + setPointerGraceArea(graceArea); + onPointerInTransitChange(true); + }, [onPointerInTransitChange]); + $iVrL9$react.useEffect(() => { + return () => handleRemoveGraceArea(); + }, [handleRemoveGraceArea]); + $iVrL9$react.useEffect(() => { + if (trigger && content) { + const handleTriggerLeave = event => handleCreateGraceArea(event, content); + const handleContentLeave = event => handleCreateGraceArea(event, trigger); + trigger.addEventListener('pointerleave', handleTriggerLeave); + content.addEventListener('pointerleave', handleContentLeave); + return () => { + trigger.removeEventListener('pointerleave', handleTriggerLeave); + content.removeEventListener('pointerleave', handleContentLeave); + }; + } + }, [trigger, content, handleCreateGraceArea, handleRemoveGraceArea]); + $iVrL9$react.useEffect(() => { + if (pointerGraceArea) { + const handleTrackPointerGrace = event => { + const target = event.target; + const pointerPosition = { + x: event.clientX, + y: event.clientY + }; + const hasEnteredTarget = (trigger === null || trigger === void 0 ? void 0 : trigger.contains(target)) || (content === null || content === void 0 ? void 0 : content.contains(target)); + const isPointerOutsideGraceArea = !$c34afbc43c90cc6f$var$isPointInPolygon(pointerPosition, pointerGraceArea); + if (hasEnteredTarget) handleRemoveGraceArea();else if (isPointerOutsideGraceArea) { + handleRemoveGraceArea(); + onClose(); + } + }; + document.addEventListener('pointermove', handleTrackPointerGrace); + return () => document.removeEventListener('pointermove', handleTrackPointerGrace); + } + }, [trigger, content, pointerGraceArea, onClose, handleRemoveGraceArea]); + return /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$TooltipContentImpl, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({}, props, { + ref: composedRefs + })); +}); +const [$c34afbc43c90cc6f$var$VisuallyHiddenContentContextProvider, $c34afbc43c90cc6f$var$useVisuallyHiddenContentContext] = $c34afbc43c90cc6f$var$createTooltipContext($c34afbc43c90cc6f$var$TOOLTIP_NAME, { + isInside: false +}); +const $c34afbc43c90cc6f$var$TooltipContentImpl = /*#__PURE__*/$iVrL9$react.forwardRef((props, forwardedRef) => { + const { + __scopeTooltip: __scopeTooltip, + children: children, + 'aria-label': ariaLabel, + onEscapeKeyDown: onEscapeKeyDown, + onPointerDownOutside: onPointerDownOutside, + ...contentProps + } = props; + const context = $c34afbc43c90cc6f$var$useTooltipContext($c34afbc43c90cc6f$var$CONTENT_NAME, __scopeTooltip); + const popperScope = $c34afbc43c90cc6f$var$usePopperScope(__scopeTooltip); + const { + onClose: onClose + } = context; // Close this tooltip if another one opens + $iVrL9$react.useEffect(() => { + document.addEventListener($c34afbc43c90cc6f$var$TOOLTIP_OPEN, onClose); + return () => document.removeEventListener($c34afbc43c90cc6f$var$TOOLTIP_OPEN, onClose); + }, [onClose]); // Close the tooltip if the trigger is scrolled + $iVrL9$react.useEffect(() => { + if (context.trigger) { + const handleScroll = event => { + const target = event.target; + if (target !== null && target !== void 0 && target.contains(context.trigger)) onClose(); + }; + window.addEventListener('scroll', handleScroll, { + capture: true + }); + return () => window.removeEventListener('scroll', handleScroll, { + capture: true + }); + } + }, [context.trigger, onClose]); + return /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactdismissablelayer.DismissableLayer, { + asChild: true, + disableOutsidePointerEvents: false, + onEscapeKeyDown: onEscapeKeyDown, + onPointerDownOutside: onPointerDownOutside, + onFocusOutside: event => event.preventDefault(), + onDismiss: onClose + }, /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpopper.Content, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({ + "data-state": context.stateAttribute + }, popperScope, contentProps, { + ref: forwardedRef, + style: { + ...contentProps.style, + '--radix-tooltip-content-transform-origin': 'var(--radix-popper-transform-origin)', + '--radix-tooltip-content-available-width': 'var(--radix-popper-available-width)', + '--radix-tooltip-content-available-height': 'var(--radix-popper-available-height)', + '--radix-tooltip-trigger-width': 'var(--radix-popper-anchor-width)', + '--radix-tooltip-trigger-height': 'var(--radix-popper-anchor-height)' + } + }), /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactslot.Slottable, null, children), /*#__PURE__*/$iVrL9$react.createElement($c34afbc43c90cc6f$var$VisuallyHiddenContentContextProvider, { + scope: __scopeTooltip, + isInside: true + }, /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactvisuallyhidden.Root, { + id: context.contentId, + role: "tooltip" + }, ariaLabel || children)))); +}); +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$e9003e2be37ec060, { + displayName: $c34afbc43c90cc6f$var$CONTENT_NAME +}); +/* ------------------------------------------------------------------------------------------------- + * TooltipArrow + * -----------------------------------------------------------------------------------------------*/ +const $c34afbc43c90cc6f$var$ARROW_NAME = 'TooltipArrow'; +const $c34afbc43c90cc6f$export$c27ee0ad710f7559 = /*#__PURE__*/$iVrL9$react.forwardRef((props, forwardedRef) => { + const { + __scopeTooltip: __scopeTooltip, + ...arrowProps + } = props; + const popperScope = $c34afbc43c90cc6f$var$usePopperScope(__scopeTooltip); + const visuallyHiddenContentContext = $c34afbc43c90cc6f$var$useVisuallyHiddenContentContext($c34afbc43c90cc6f$var$ARROW_NAME, __scopeTooltip); // if the arrow is inside the `VisuallyHidden`, we don't want to render it all to + // prevent issues in positioning the arrow due to the duplicate + return visuallyHiddenContentContext.isInside ? null : /*#__PURE__*/$iVrL9$react.createElement($iVrL9$radixuireactpopper.Arrow, $parcel$interopDefault($iVrL9$babelruntimehelpersextends)({}, popperScope, arrowProps, { + ref: forwardedRef + })); +}); +/*#__PURE__*/ +Object.assign($c34afbc43c90cc6f$export$c27ee0ad710f7559, { + displayName: $c34afbc43c90cc6f$var$ARROW_NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +function $c34afbc43c90cc6f$var$getExitSideFromRect(point, rect) { + const top = Math.abs(rect.top - point.y); + const bottom = Math.abs(rect.bottom - point.y); + const right = Math.abs(rect.right - point.x); + const left = Math.abs(rect.left - point.x); + switch (Math.min(top, bottom, right, left)) { + case left: + return 'left'; + case right: + return 'right'; + case top: + return 'top'; + case bottom: + return 'bottom'; + default: + throw new Error('unreachable'); + } +} +function $c34afbc43c90cc6f$var$getPaddedExitPoints(exitPoint, exitSide) { + let padding = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 5; + const paddedExitPoints = []; + switch (exitSide) { + case 'top': + paddedExitPoints.push({ + x: exitPoint.x - padding, + y: exitPoint.y + padding + }, { + x: exitPoint.x + padding, + y: exitPoint.y + padding + }); + break; + case 'bottom': + paddedExitPoints.push({ + x: exitPoint.x - padding, + y: exitPoint.y - padding + }, { + x: exitPoint.x + padding, + y: exitPoint.y - padding + }); + break; + case 'left': + paddedExitPoints.push({ + x: exitPoint.x + padding, + y: exitPoint.y - padding + }, { + x: exitPoint.x + padding, + y: exitPoint.y + padding + }); + break; + case 'right': + paddedExitPoints.push({ + x: exitPoint.x - padding, + y: exitPoint.y - padding + }, { + x: exitPoint.x - padding, + y: exitPoint.y + padding + }); + break; + } + return paddedExitPoints; +} +function $c34afbc43c90cc6f$var$getPointsFromRect(rect) { + const { + top: top, + right: right, + bottom: bottom, + left: left + } = rect; + return [{ + x: left, + y: top + }, { + x: right, + y: top + }, { + x: right, + y: bottom + }, { + x: left, + y: bottom + }]; +} // Determine if a point is inside of a polygon. +// Based on https://github.com/substack/point-in-polygon +function $c34afbc43c90cc6f$var$isPointInPolygon(point, polygon) { + const { + x: x, + y: y + } = point; + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x; + const yi = polygon[i].y; + const xj = polygon[j].x; + const yj = polygon[j].y; // prettier-ignore + const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; +} // Returns a new array of points representing the convex hull of the given set of points. +// https://www.nayuki.io/page/convex-hull-algorithm +function $c34afbc43c90cc6f$var$getHull(points) { + const newPoints = points.slice(); + newPoints.sort((a, b) => { + if (a.x < b.x) return -1;else if (a.x > b.x) return 1;else if (a.y < b.y) return -1;else if (a.y > b.y) return 1;else return 0; + }); + return $c34afbc43c90cc6f$var$getHullPresorted(newPoints); +} // Returns the convex hull, assuming that each points[i] <= points[i + 1]. Runs in O(n) time. +function $c34afbc43c90cc6f$var$getHullPresorted(points) { + if (points.length <= 1) return points.slice(); + const upperHull = []; + for (let i = 0; i < points.length; i++) { + const p = points[i]; + while (upperHull.length >= 2) { + const q = upperHull[upperHull.length - 1]; + const r = upperHull[upperHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();else break; + } + upperHull.push(p); + } + upperHull.pop(); + const lowerHull = []; + for (let i1 = points.length - 1; i1 >= 0; i1--) { + const p = points[i1]; + while (lowerHull.length >= 2) { + const q = lowerHull[lowerHull.length - 1]; + const r = lowerHull[lowerHull.length - 2]; + if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();else break; + } + lowerHull.push(p); + } + lowerHull.pop(); + if (upperHull.length === 1 && lowerHull.length === 1 && upperHull[0].x === lowerHull[0].x && upperHull[0].y === lowerHull[0].y) return upperHull;else return upperHull.concat(lowerHull); +} +const $c34afbc43c90cc6f$export$2881499e37b75b9a = $c34afbc43c90cc6f$export$f78649fb9ca566b8; +const $c34afbc43c90cc6f$export$be92b6f5f03c0fe9 = $c34afbc43c90cc6f$export$28c660c63b792dea; +const $c34afbc43c90cc6f$export$41fb9f06171c75f4 = $c34afbc43c90cc6f$export$8c610744efcf8a1d; +const $c34afbc43c90cc6f$export$602eac185826482c = $c34afbc43c90cc6f$export$7b36b8f925ab7497; +const $c34afbc43c90cc6f$export$7c6e2c02157bb7d2 = $c34afbc43c90cc6f$export$e9003e2be37ec060; +const $c34afbc43c90cc6f$export$21b07c8f274aebd5 = $c34afbc43c90cc6f$export$c27ee0ad710f7559; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js": +/*!****************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js ***! + \****************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $92muK$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useCallbackRef", () => $28e03942f763e819$export$25bec8c6f54ee79a); + +/** + * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a + * prop or avoid re-executing effects when passed as a dependency + */ +function $28e03942f763e819$export$25bec8c6f54ee79a(callback) { + const callbackRef = $92muK$react.useRef(callback); + $92muK$react.useEffect(() => { + callbackRef.current = callback; + }); // https://github.com/facebook/react/issues/19240 + return $92muK$react.useMemo(() => function () { + var _callbackRef$current; + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + return (_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef, ...args); + }, []); +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js": +/*!**********************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-use-controllable-state/dist/index.js ***! + \**********************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $ijazI$react = __webpack_require__(/*! react */ "react"); +var $ijazI$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useControllableState", () => $b84d42d44371bff7$export$6f32135080cb4c3); +function $b84d42d44371bff7$export$6f32135080cb4c3(_ref) { + let { + prop: prop, + defaultProp: defaultProp, + onChange = () => {} + } = _ref; + const [uncontrolledProp, setUncontrolledProp] = $b84d42d44371bff7$var$useUncontrolledState({ + defaultProp: defaultProp, + onChange: onChange + }); + const isControlled = prop !== undefined; + const value1 = isControlled ? prop : uncontrolledProp; + const handleChange = $ijazI$radixuireactusecallbackref.useCallbackRef(onChange); + const setValue = $ijazI$react.useCallback(nextValue => { + if (isControlled) { + const setter = nextValue; + const value = typeof nextValue === 'function' ? setter(prop) : nextValue; + if (value !== prop) handleChange(value); + } else setUncontrolledProp(nextValue); + }, [isControlled, prop, setUncontrolledProp, handleChange]); + return [value1, setValue]; +} +function $b84d42d44371bff7$var$useUncontrolledState(_ref2) { + let { + defaultProp: defaultProp, + onChange: onChange + } = _ref2; + const uncontrolledState = $ijazI$react.useState(defaultProp); + const [value] = uncontrolledState; + const prevValueRef = $ijazI$react.useRef(value); + const handleChange = $ijazI$radixuireactusecallbackref.useCallbackRef(onChange); + $ijazI$react.useEffect(() => { + if (prevValueRef.current !== value) { + handleChange(value); + prevValueRef.current = value; + } + }, [value, prevValueRef, handleChange]); + return uncontrolledState; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-use-escape-keydown/dist/index.js": +/*!******************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-use-escape-keydown/dist/index.js ***! + \******************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $b0gz3$react = __webpack_require__(/*! react */ "react"); +var $b0gz3$radixuireactusecallbackref = __webpack_require__(/*! @radix-ui/react-use-callback-ref */ "../../../node_modules/@radix-ui/react-use-callback-ref/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useEscapeKeydown", () => $24c84e9f83c4454f$export$3a72a57244d6e765); + +/** + * Listens for when the escape key is down + */ +function $24c84e9f83c4454f$export$3a72a57244d6e765(onEscapeKeyDownProp) { + let ownerDocument = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : globalThis === null || globalThis === void 0 ? void 0 : globalThis.document; + const onEscapeKeyDown = $b0gz3$radixuireactusecallbackref.useCallbackRef(onEscapeKeyDownProp); + $b0gz3$react.useEffect(() => { + const handleKeyDown = event => { + if (event.key === 'Escape') onEscapeKeyDown(event); + }; + ownerDocument.addEventListener('keydown', handleKeyDown); + return () => ownerDocument.removeEventListener('keydown', handleKeyDown); + }, [onEscapeKeyDown, ownerDocument]); +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js": +/*!*****************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js ***! + \*****************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $caHyQ$react = __webpack_require__(/*! react */ "react"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useLayoutEffect", () => $ca21affb0542a8a4$export$e5c5a5f917a5871c); + +/** + * On the server, React emits a warning when calling `useLayoutEffect`. + * This is because neither `useLayoutEffect` nor `useEffect` run on the server. + * We use this safe version which suppresses the warning by replacing it with a noop on the server. + * + * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect + */ +const $ca21affb0542a8a4$export$e5c5a5f917a5871c = Boolean(globalThis === null || globalThis === void 0 ? void 0 : globalThis.document) ? $caHyQ$react.useLayoutEffect : () => {}; + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-use-size/dist/index.js": +/*!********************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-use-size/dist/index.js ***! + \********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $ksDzM$react = __webpack_require__(/*! react */ "react"); +var $ksDzM$radixuireactuselayouteffect = __webpack_require__(/*! @radix-ui/react-use-layout-effect */ "../../../node_modules/@radix-ui/react-use-layout-effect/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +$parcel$export(module.exports, "useSize", () => $d2c1d285af17635b$export$1ab7ae714698c4b8); +function $d2c1d285af17635b$export$1ab7ae714698c4b8(element) { + const [size, setSize] = $ksDzM$react.useState(undefined); + $ksDzM$radixuireactuselayouteffect.useLayoutEffect(() => { + if (element) { + // provide size as early as possible + setSize({ + width: element.offsetWidth, + height: element.offsetHeight + }); + const resizeObserver = new ResizeObserver(entries => { + if (!Array.isArray(entries)) return; + // Since we only observe the one element, we don't need to loop over the + // array + if (!entries.length) return; + const entry = entries[0]; + let width; + let height; + if ('borderBoxSize' in entry) { + const borderSizeEntry = entry['borderBoxSize']; // iron out differences between browsers + const borderSize = Array.isArray(borderSizeEntry) ? borderSizeEntry[0] : borderSizeEntry; + width = borderSize['inlineSize']; + height = borderSize['blockSize']; + } else { + // for browsers that don't support `borderBoxSize` + // we calculate it ourselves to get the correct border box. + width = element.offsetWidth; + height = element.offsetHeight; + } + setSize({ + width: width, + height: height + }); + }); + resizeObserver.observe(element, { + box: 'border-box' + }); + return () => resizeObserver.unobserve(element); + } else + // We only want to reset to `undefined` when the element becomes `null`, + // not if it changes to another element. + setSize(undefined); + }, [element]); + return size; +} + +/***/ }), + +/***/ "../../../node_modules/@radix-ui/react-visually-hidden/dist/index.js": +/*!***************************************************************************!*\ + !*** ../../../node_modules/@radix-ui/react-visually-hidden/dist/index.js ***! + \***************************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var $awrN2$babelruntimehelpersextends = __webpack_require__(/*! @babel/runtime/helpers/extends */ "../../../node_modules/@babel/runtime/helpers/extends.js"); +var $awrN2$react = __webpack_require__(/*! react */ "react"); +var $awrN2$radixuireactprimitive = __webpack_require__(/*! @radix-ui/react-primitive */ "../../../node_modules/@radix-ui/react-primitive/dist/index.js"); +function $parcel$export(e, n, v, s) { + Object.defineProperty(e, n, { + get: v, + set: s, + enumerable: true, + configurable: true + }); +} +function $parcel$interopDefault(a) { + return a && a.__esModule ? a.default : a; +} +$parcel$export(module.exports, "VisuallyHidden", () => $685371e9c20848e2$export$439d29a4e110a164); +$parcel$export(module.exports, "Root", () => $685371e9c20848e2$export$be92b6f5f03c0fe9); + +/* ------------------------------------------------------------------------------------------------- + * VisuallyHidden + * -----------------------------------------------------------------------------------------------*/ +const $685371e9c20848e2$var$NAME = 'VisuallyHidden'; +const $685371e9c20848e2$export$439d29a4e110a164 = /*#__PURE__*/$awrN2$react.forwardRef((props, forwardedRef) => { + return /*#__PURE__*/$awrN2$react.createElement($awrN2$radixuireactprimitive.Primitive.span, $parcel$interopDefault($awrN2$babelruntimehelpersextends)({}, props, { + ref: forwardedRef, + style: { + // See: https://github.com/twbs/bootstrap/blob/master/scss/mixins/_screen-reader.scss + position: 'absolute', + border: 0, + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + wordWrap: 'normal', + ...props.style + } + })); +}); +/*#__PURE__*/ +Object.assign($685371e9c20848e2$export$439d29a4e110a164, { + displayName: $685371e9c20848e2$var$NAME +}); +/* -----------------------------------------------------------------------------------------------*/ +const $685371e9c20848e2$export$be92b6f5f03c0fe9 = $685371e9c20848e2$export$439d29a4e110a164; + +/***/ }), + +/***/ "../../../node_modules/aria-hidden/dist/es2015/index.js": +/*!**************************************************************!*\ + !*** ../../../node_modules/aria-hidden/dist/es2015/index.js ***! + \**************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.suppressOthers = exports.supportsInert = exports.inertOthers = exports.hideOthers = void 0; +var getDefaultParent = function (originalTarget) { + if (typeof document === 'undefined') { + return null; + } + var sampleTarget = Array.isArray(originalTarget) ? originalTarget[0] : originalTarget; + return sampleTarget.ownerDocument.body; +}; +var counterMap = new WeakMap(); +var uncontrolledNodes = new WeakMap(); +var markerMap = {}; +var lockCount = 0; +var unwrapHost = function (node) { + return node && (node.host || unwrapHost(node.parentNode)); +}; +var correctTargets = function (parent, targets) { + return targets.map(function (target) { + if (parent.contains(target)) { + return target; + } + var correctedTarget = unwrapHost(target); + if (correctedTarget && parent.contains(correctedTarget)) { + return correctedTarget; + } + console.error('aria-hidden', target, 'in not contained inside', parent, '. Doing nothing'); + return null; + }).filter(function (x) { + return Boolean(x); + }); +}; +/** + * Marks everything except given node(or nodes) as aria-hidden + * @param {Element | Element[]} originalTarget - elements to keep on the page + * @param [parentNode] - top element, defaults to document.body + * @param {String} [markerName] - a special attribute to mark every node + * @param {String} [controlAttribute] - html Attribute to control + * @return {Undo} undo command + */ +var applyAttributeToOthers = function (originalTarget, parentNode, markerName, controlAttribute) { + var targets = correctTargets(parentNode, Array.isArray(originalTarget) ? originalTarget : [originalTarget]); + if (!markerMap[markerName]) { + markerMap[markerName] = new WeakMap(); + } + var markerCounter = markerMap[markerName]; + var hiddenNodes = []; + var elementsToKeep = new Set(); + var elementsToStop = new Set(targets); + var keep = function (el) { + if (!el || elementsToKeep.has(el)) { + return; + } + elementsToKeep.add(el); + keep(el.parentNode); + }; + targets.forEach(keep); + var deep = function (parent) { + if (!parent || elementsToStop.has(parent)) { + return; + } + Array.prototype.forEach.call(parent.children, function (node) { + if (elementsToKeep.has(node)) { + deep(node); + } else { + var attr = node.getAttribute(controlAttribute); + var alreadyHidden = attr !== null && attr !== 'false'; + var counterValue = (counterMap.get(node) || 0) + 1; + var markerValue = (markerCounter.get(node) || 0) + 1; + counterMap.set(node, counterValue); + markerCounter.set(node, markerValue); + hiddenNodes.push(node); + if (counterValue === 1 && alreadyHidden) { + uncontrolledNodes.set(node, true); + } + if (markerValue === 1) { + node.setAttribute(markerName, 'true'); + } + if (!alreadyHidden) { + node.setAttribute(controlAttribute, 'true'); + } + } + }); + }; + deep(parentNode); + elementsToKeep.clear(); + lockCount++; + return function () { + hiddenNodes.forEach(function (node) { + var counterValue = counterMap.get(node) - 1; + var markerValue = markerCounter.get(node) - 1; + counterMap.set(node, counterValue); + markerCounter.set(node, markerValue); + if (!counterValue) { + if (!uncontrolledNodes.has(node)) { + node.removeAttribute(controlAttribute); + } + uncontrolledNodes.delete(node); + } + if (!markerValue) { + node.removeAttribute(markerName); + } + }); + lockCount--; + if (!lockCount) { + // clear + counterMap = new WeakMap(); + counterMap = new WeakMap(); + uncontrolledNodes = new WeakMap(); + markerMap = {}; + } + }; +}; +/** + * Marks everything except given node(or nodes) as aria-hidden + * @param {Element | Element[]} originalTarget - elements to keep on the page + * @param [parentNode] - top element, defaults to document.body + * @param {String} [markerName] - a special attribute to mark every node + * @return {Undo} undo command + */ +var hideOthers = function (originalTarget, parentNode, markerName) { + if (markerName === void 0) { + markerName = 'data-aria-hidden'; + } + var targets = Array.from(Array.isArray(originalTarget) ? originalTarget : [originalTarget]); + var activeParentNode = parentNode || getDefaultParent(originalTarget); + if (!activeParentNode) { + return function () { + return null; + }; + } + // we should not hide ariaLive elements - https://github.com/theKashey/aria-hidden/issues/10 + targets.push.apply(targets, Array.from(activeParentNode.querySelectorAll('[aria-live]'))); + return applyAttributeToOthers(targets, activeParentNode, markerName, 'aria-hidden'); +}; +/** + * Marks everything except given node(or nodes) as inert + * @param {Element | Element[]} originalTarget - elements to keep on the page + * @param [parentNode] - top element, defaults to document.body + * @param {String} [markerName] - a special attribute to mark every node + * @return {Undo} undo command + */ +exports.hideOthers = hideOthers; +var inertOthers = function (originalTarget, parentNode, markerName) { + if (markerName === void 0) { + markerName = 'data-inert-ed'; + } + var activeParentNode = parentNode || getDefaultParent(originalTarget); + if (!activeParentNode) { + return function () { + return null; + }; + } + return applyAttributeToOthers(originalTarget, activeParentNode, markerName, 'inert'); +}; +/** + * @returns if current browser supports inert + */ +exports.inertOthers = inertOthers; +var supportsInert = function () { + return typeof HTMLElement !== 'undefined' && HTMLElement.prototype.hasOwnProperty('inert'); +}; +/** + * Automatic function to "suppress" DOM elements - _hide_ or _inert_ in the best possible way + * @param {Element | Element[]} originalTarget - elements to keep on the page + * @param [parentNode] - top element, defaults to document.body + * @param {String} [markerName] - a special attribute to mark every node + * @return {Undo} undo command + */ +exports.supportsInert = supportsInert; +var suppressOthers = function (originalTarget, parentNode, markerName) { + if (markerName === void 0) { + markerName = 'data-suppressed'; + } + return (supportsInert() ? inertOthers : hideOthers)(originalTarget, parentNode, markerName); +}; +exports.suppressOthers = suppressOthers; + +/***/ }), + +/***/ "../../../node_modules/clsx/dist/clsx.m.js": +/*!*************************************************!*\ + !*** ../../../node_modules/clsx/dist/clsx.m.js ***! + \*************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.clsx = clsx; +exports["default"] = void 0; +function r(e) { + var t, + f, + n = ""; + if ("string" == typeof e || "number" == typeof e) n += e;else if ("object" == typeof e) if (Array.isArray(e)) for (t = 0; t < e.length; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);else for (t in e) e[t] && (n && (n += " "), n += t); + return n; +} +function clsx() { + for (var e, t, f = 0, n = ""; f < arguments.length;) (e = arguments[f++]) && (t = r(e)) && (n && (n += " "), n += t); + return n; +} +var _default = clsx; +exports["default"] = _default; + +/***/ }), + +/***/ "../../../node_modules/copy-to-clipboard/index.js": +/*!********************************************************!*\ + !*** ../../../node_modules/copy-to-clipboard/index.js ***! + \********************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + + +var deselectCurrent = __webpack_require__(/*! toggle-selection */ "../../../node_modules/toggle-selection/index.js"); +var clipboardToIE11Formatting = { + "text/plain": "Text", + "text/html": "Url", + "default": "Text" +}; +var defaultMessage = "Copy to clipboard: #{key}, Enter"; +function format(message) { + var copyKey = (/mac os x/i.test(navigator.userAgent) ? "⌘" : "Ctrl") + "+C"; + return message.replace(/#{\s*key\s*}/g, copyKey); +} +function copy(text, options) { + var debug, + message, + reselectPrevious, + range, + selection, + mark, + success = false; + if (!options) { + options = {}; + } + debug = options.debug || false; + try { + reselectPrevious = deselectCurrent(); + range = document.createRange(); + selection = document.getSelection(); + mark = document.createElement("span"); + mark.textContent = text; + // avoid screen readers from reading out loud the text + mark.ariaHidden = "true"; + // reset user styles for span element + mark.style.all = "unset"; + // prevents scrolling to the end of the page + mark.style.position = "fixed"; + mark.style.top = 0; + mark.style.clip = "rect(0, 0, 0, 0)"; + // used to preserve spaces and line breaks + mark.style.whiteSpace = "pre"; + // do not inherit user-select (it may be `none`) + mark.style.webkitUserSelect = "text"; + mark.style.MozUserSelect = "text"; + mark.style.msUserSelect = "text"; + mark.style.userSelect = "text"; + mark.addEventListener("copy", function (e) { + e.stopPropagation(); + if (options.format) { + e.preventDefault(); + if (typeof e.clipboardData === "undefined") { + // IE 11 + debug && console.warn("unable to use e.clipboardData"); + debug && console.warn("trying IE specific stuff"); + window.clipboardData.clearData(); + var format = clipboardToIE11Formatting[options.format] || clipboardToIE11Formatting["default"]; + window.clipboardData.setData(format, text); + } else { + // all other browsers + e.clipboardData.clearData(); + e.clipboardData.setData(options.format, text); + } + } + if (options.onCopy) { + e.preventDefault(); + options.onCopy(e.clipboardData); + } + }); + document.body.appendChild(mark); + range.selectNodeContents(mark); + selection.addRange(range); + var successful = document.execCommand("copy"); + if (!successful) { + throw new Error("copy command was unsuccessful"); + } + success = true; + } catch (err) { + debug && console.error("unable to copy using execCommand: ", err); + debug && console.warn("trying IE specific stuff"); + try { + window.clipboardData.setData(options.format || "text", text); + options.onCopy && options.onCopy(window.clipboardData); + success = true; + } catch (err) { + debug && console.error("unable to copy using clipboardData: ", err); + debug && console.error("falling back to prompt"); + message = format("message" in options ? options.message : defaultMessage); + window.prompt(message, text); + } + } finally { + if (selection) { + if (typeof selection.removeRange == "function") { + selection.removeRange(range); + } else { + selection.removeAllRanges(); + } + } + if (mark) { + document.body.removeChild(mark); + } + reselectPrevious(); + } + return success; +} +module.exports = copy; + +/***/ }), + +/***/ "../../../node_modules/detect-node-es/esm/browser.js": +/*!***********************************************************!*\ + !*** ../../../node_modules/detect-node-es/esm/browser.js ***! + \***********************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isNode = void 0; +const isNode = false; +exports.isNode = isNode; + +/***/ }), + +/***/ "../../../node_modules/framer-motion/dist/cjs/index.js": +/*!*************************************************************!*\ + !*** ../../../node_modules/framer-motion/dist/cjs/index.js ***! + \*************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var tslib = __webpack_require__(/*! tslib */ "../../../node_modules/tslib/tslib.es6.js"); +var React = __webpack_require__(/*! react */ "react"); +var heyListen = __webpack_require__(/*! hey-listen */ "../../../node_modules/hey-listen/dist/hey-listen.es.js"); +var styleValueTypes = __webpack_require__(/*! style-value-types */ "../../../node_modules/style-value-types/dist/valueTypes.cjs.js"); +var popmotion = __webpack_require__(/*! popmotion */ "../../../node_modules/popmotion/dist/popmotion.cjs.js"); +var sync = __webpack_require__(/*! framesync */ "../../../node_modules/framesync/dist/framesync.cjs.js"); +var dom = __webpack_require__(/*! @motionone/dom */ "../../../node_modules/@motionone/dom/dist/index.es.js"); +function _interopDefaultLegacy(e) { + return e && typeof e === 'object' && 'default' in e ? e : { + 'default': e + }; +} +function _interopNamespace(e) { + if (e && e.__esModule) return e; + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { + return e[k]; + } + }); + } + }); + } + n["default"] = e; + return Object.freeze(n); +} +var React__namespace = /*#__PURE__*/_interopNamespace(React); +var React__default = /*#__PURE__*/_interopDefaultLegacy(React); +var sync__default = /*#__PURE__*/_interopDefaultLegacy(sync); + +/** + * Browser-safe usage of process + */ +var defaultEnvironment = "production"; +var env = typeof process === "undefined" || process.env === undefined ? defaultEnvironment : "development" || 0; +var createDefinition = function (propNames) { + return { + isEnabled: function (props) { + return propNames.some(function (name) { + return !!props[name]; + }); + } + }; +}; +var featureDefinitions = { + measureLayout: createDefinition(["layout", "layoutId", "drag"]), + animation: createDefinition(["animate", "exit", "variants", "whileHover", "whileTap", "whileFocus", "whileDrag", "whileInView"]), + exit: createDefinition(["exit"]), + drag: createDefinition(["drag", "dragControls"]), + focus: createDefinition(["whileFocus"]), + hover: createDefinition(["whileHover", "onHoverStart", "onHoverEnd"]), + tap: createDefinition(["whileTap", "onTap", "onTapStart", "onTapCancel"]), + pan: createDefinition(["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"]), + inView: createDefinition(["whileInView", "onViewportEnter", "onViewportLeave"]) +}; +function loadFeatures(features) { + for (var key in features) { + if (features[key] === null) continue; + if (key === "projectionNodeConstructor") { + featureDefinitions.projectionNodeConstructor = features[key]; + } else { + featureDefinitions[key].Component = features[key]; + } + } +} +var LazyContext = React.createContext({ + strict: false +}); +var featureNames = Object.keys(featureDefinitions); +var numFeatures = featureNames.length; +/** + * Load features via renderless components based on the provided MotionProps. + */ +function useFeatures(props, visualElement, preloadedFeatures) { + var features = []; + var lazyContext = React.useContext(LazyContext); + if (!visualElement) return null; + /** + * If we're in development mode, check to make sure we're not rendering a motion component + * as a child of LazyMotion, as this will break the file-size benefits of using it. + */ + if (env !== "production" && preloadedFeatures && lazyContext.strict) { + heyListen.invariant(false, "You have rendered a `motion` component within a `LazyMotion` component. This will break tree shaking. Import and render a `m` component instead."); + } + for (var i = 0; i < numFeatures; i++) { + var name_1 = featureNames[i]; + var _a = featureDefinitions[name_1], + isEnabled = _a.isEnabled, + Component = _a.Component; + /** + * It might be possible in the future to use this moment to + * dynamically request functionality. In initial tests this + * was producing a lot of duplication amongst bundles. + */ + if (isEnabled(props) && Component) { + features.push(React__namespace.createElement(Component, tslib.__assign({ + key: name_1 + }, props, { + visualElement: visualElement + }))); + } + } + return features; +} + +/** + * @public + */ +var MotionConfigContext = React.createContext({ + transformPagePoint: function (p) { + return p; + }, + isStatic: false, + reducedMotion: "never" +}); +var MotionContext = React.createContext({}); +function useVisualElementContext() { + return React.useContext(MotionContext).visualElement; +} + +/** + * @public + */ +var PresenceContext = React.createContext(null); +var isBrowser = typeof document !== "undefined"; +var useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect; + +// Does this device prefer reduced motion? Returns `null` server-side. +var prefersReducedMotion = { + current: null +}; +var hasDetected = false; +function initPrefersReducedMotion() { + hasDetected = true; + if (!isBrowser) return; + if (window.matchMedia) { + var motionMediaQuery_1 = window.matchMedia("(prefers-reduced-motion)"); + var setReducedMotionPreferences = function () { + return prefersReducedMotion.current = motionMediaQuery_1.matches; + }; + motionMediaQuery_1.addListener(setReducedMotionPreferences); + setReducedMotionPreferences(); + } else { + prefersReducedMotion.current = false; + } +} +/** + * A hook that returns `true` if we should be using reduced motion based on the current device's Reduced Motion setting. + * + * This can be used to implement changes to your UI based on Reduced Motion. For instance, replacing motion-sickness inducing + * `x`/`y` animations with `opacity`, disabling the autoplay of background videos, or turning off parallax motion. + * + * It will actively respond to changes and re-render your components with the latest setting. + * + * ```jsx + * export function Sidebar({ isOpen }) { + * const shouldReduceMotion = useReducedMotion() + * const closedX = shouldReduceMotion ? 0 : "-100%" + * + * return ( + * + * ) + * } + * ``` + * + * @return boolean + * + * @public + */ +function useReducedMotion() { + /** + * Lazy initialisation of prefersReducedMotion + */ + !hasDetected && initPrefersReducedMotion(); + var _a = tslib.__read(React.useState(prefersReducedMotion.current), 1), + shouldReduceMotion = _a[0]; + /** + * TODO See if people miss automatically updating shouldReduceMotion setting + */ + return shouldReduceMotion; +} +function useReducedMotionConfig() { + var reducedMotionPreference = useReducedMotion(); + var reducedMotion = React.useContext(MotionConfigContext).reducedMotion; + if (reducedMotion === "never") { + return false; + } else if (reducedMotion === "always") { + return true; + } else { + return reducedMotionPreference; + } +} +function useVisualElement(Component, visualState, props, createVisualElement) { + var lazyContext = React.useContext(LazyContext); + var parent = useVisualElementContext(); + var presenceContext = React.useContext(PresenceContext); + var shouldReduceMotion = useReducedMotionConfig(); + var visualElementRef = React.useRef(undefined); + /** + * If we haven't preloaded a renderer, check to see if we have one lazy-loaded + */ + if (!createVisualElement) createVisualElement = lazyContext.renderer; + if (!visualElementRef.current && createVisualElement) { + visualElementRef.current = createVisualElement(Component, { + visualState: visualState, + parent: parent, + props: props, + presenceId: presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.id, + blockInitialAnimation: (presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.initial) === false, + shouldReduceMotion: shouldReduceMotion + }); + } + var visualElement = visualElementRef.current; + useIsomorphicLayoutEffect(function () { + visualElement === null || visualElement === void 0 ? void 0 : visualElement.syncRender(); + }); + React.useEffect(function () { + var _a; + (_a = visualElement === null || visualElement === void 0 ? void 0 : visualElement.animationState) === null || _a === void 0 ? void 0 : _a.animateChanges(); + }); + useIsomorphicLayoutEffect(function () { + return function () { + return visualElement === null || visualElement === void 0 ? void 0 : visualElement.notifyUnmount(); + }; + }, []); + return visualElement; +} +function isRefObject(ref) { + return typeof ref === "object" && Object.prototype.hasOwnProperty.call(ref, "current"); +} + +/** + * Creates a ref function that, when called, hydrates the provided + * external ref and VisualElement. + */ +function useMotionRef(visualState, visualElement, externalRef) { + return React.useCallback(function (instance) { + var _a; + instance && ((_a = visualState.mount) === null || _a === void 0 ? void 0 : _a.call(visualState, instance)); + if (visualElement) { + instance ? visualElement.mount(instance) : visualElement.unmount(); + } + if (externalRef) { + if (typeof externalRef === "function") { + externalRef(instance); + } else if (isRefObject(externalRef)) { + externalRef.current = instance; + } + } + }, + /** + * Only pass a new ref callback to React if we've received a visual element + * factory. Otherwise we'll be mounting/remounting every time externalRef + * or other dependencies change. + */ + [visualElement]); +} + +/** + * Decides if the supplied variable is an array of variant labels + */ +function isVariantLabels(v) { + return Array.isArray(v); +} +/** + * Decides if the supplied variable is variant label + */ +function isVariantLabel(v) { + return typeof v === "string" || isVariantLabels(v); +} +/** + * Creates an object containing the latest state of every MotionValue on a VisualElement + */ +function getCurrent(visualElement) { + var current = {}; + visualElement.forEachValue(function (value, key) { + return current[key] = value.get(); + }); + return current; +} +/** + * Creates an object containing the latest velocity of every MotionValue on a VisualElement + */ +function getVelocity$1(visualElement) { + var velocity = {}; + visualElement.forEachValue(function (value, key) { + return velocity[key] = value.getVelocity(); + }); + return velocity; +} +function resolveVariantFromProps(props, definition, custom, currentValues, currentVelocity) { + var _a; + if (currentValues === void 0) { + currentValues = {}; + } + if (currentVelocity === void 0) { + currentVelocity = {}; + } + /** + * If the variant definition is a function, resolve. + */ + if (typeof definition === "function") { + definition = definition(custom !== null && custom !== void 0 ? custom : props.custom, currentValues, currentVelocity); + } + /** + * If the variant definition is a variant label, or + * the function returned a variant label, resolve. + */ + if (typeof definition === "string") { + definition = (_a = props.variants) === null || _a === void 0 ? void 0 : _a[definition]; + } + /** + * At this point we've resolved both functions and variant labels, + * but the resolved variant label might itself have been a function. + * If so, resolve. This can only have returned a valid target object. + */ + if (typeof definition === "function") { + definition = definition(custom !== null && custom !== void 0 ? custom : props.custom, currentValues, currentVelocity); + } + return definition; +} +function resolveVariant(visualElement, definition, custom) { + var props = visualElement.getProps(); + return resolveVariantFromProps(props, definition, custom !== null && custom !== void 0 ? custom : props.custom, getCurrent(visualElement), getVelocity$1(visualElement)); +} +function checkIfControllingVariants(props) { + var _a; + return typeof ((_a = props.animate) === null || _a === void 0 ? void 0 : _a.start) === "function" || isVariantLabel(props.initial) || isVariantLabel(props.animate) || isVariantLabel(props.whileHover) || isVariantLabel(props.whileDrag) || isVariantLabel(props.whileTap) || isVariantLabel(props.whileFocus) || isVariantLabel(props.exit); +} +function checkIfVariantNode(props) { + return Boolean(checkIfControllingVariants(props) || props.variants); +} +function getCurrentTreeVariants(props, context) { + if (checkIfControllingVariants(props)) { + var initial = props.initial, + animate = props.animate; + return { + initial: initial === false || isVariantLabel(initial) ? initial : undefined, + animate: isVariantLabel(animate) ? animate : undefined + }; + } + return props.inherit !== false ? context : {}; +} +function useCreateMotionContext(props) { + var _a = getCurrentTreeVariants(props, React.useContext(MotionContext)), + initial = _a.initial, + animate = _a.animate; + return React.useMemo(function () { + return { + initial: initial, + animate: animate + }; + }, [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]); +} +function variantLabelsAsDependency(prop) { + return Array.isArray(prop) ? prop.join(" ") : prop; +} + +/** + * Creates a constant value over the lifecycle of a component. + * + * Even if `useMemo` is provided an empty array as its final argument, it doesn't offer + * a guarantee that it won't re-run for performance reasons later on. By using `useConstant` + * you can ensure that initialisers don't execute twice or more. + */ +function useConstant(init) { + var ref = React.useRef(null); + if (ref.current === null) { + ref.current = init(); + } + return ref.current; +} + +/** + * This should only ever be modified on the client otherwise it'll + * persist through server requests. If we need instanced states we + * could lazy-init via root. + */ +var globalProjectionState = { + /** + * Global flag as to whether the tree has animated since the last time + * we resized the window + */ + hasAnimatedSinceResize: true, + /** + * We set this to true once, on the first update. Any nodes added to the tree beyond that + * update will be given a `data-projection-id` attribute. + */ + hasEverUpdated: false +}; +var id$1 = 1; +function useProjectionId() { + return useConstant(function () { + if (globalProjectionState.hasEverUpdated) { + return id$1++; + } + }); +} +var LayoutGroupContext = React.createContext({}); + +/** + * Internal, exported only for usage in Framer + */ +var SwitchLayoutGroupContext = React.createContext({}); +function useProjection(projectionId, _a, visualElement, ProjectionNodeConstructor) { + var _b; + var layoutId = _a.layoutId, + layout = _a.layout, + drag = _a.drag, + dragConstraints = _a.dragConstraints, + layoutScroll = _a.layoutScroll; + var initialPromotionConfig = React.useContext(SwitchLayoutGroupContext); + if (!ProjectionNodeConstructor || !visualElement || (visualElement === null || visualElement === void 0 ? void 0 : visualElement.projection)) { + return; + } + visualElement.projection = new ProjectionNodeConstructor(projectionId, visualElement.getLatestValues(), (_b = visualElement.parent) === null || _b === void 0 ? void 0 : _b.projection); + visualElement.projection.setOptions({ + layoutId: layoutId, + layout: layout, + alwaysMeasureLayout: Boolean(drag) || dragConstraints && isRefObject(dragConstraints), + visualElement: visualElement, + scheduleRender: function () { + return visualElement.scheduleRender(); + }, + /** + * TODO: Update options in an effect. This could be tricky as it'll be too late + * to update by the time layout animations run. + * We also need to fix this safeToRemove by linking it up to the one returned by usePresence, + * ensuring it gets called if there's no potential layout animations. + * + */ + animationType: typeof layout === "string" ? layout : "both", + initialPromotionConfig: initialPromotionConfig, + layoutScroll: layoutScroll + }); +} +var VisualElementHandler = /** @class */function (_super) { + tslib.__extends(VisualElementHandler, _super); + function VisualElementHandler() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Update visual element props as soon as we know this update is going to be commited. + */ + VisualElementHandler.prototype.getSnapshotBeforeUpdate = function () { + this.updateProps(); + return null; + }; + VisualElementHandler.prototype.componentDidUpdate = function () {}; + VisualElementHandler.prototype.updateProps = function () { + var _a = this.props, + visualElement = _a.visualElement, + props = _a.props; + if (visualElement) visualElement.setProps(props); + }; + VisualElementHandler.prototype.render = function () { + return this.props.children; + }; + return VisualElementHandler; +}(React__default["default"].Component); + +/** + * Create a `motion` component. + * + * This function accepts a Component argument, which can be either a string (ie "div" + * for `motion.div`), or an actual React component. + * + * Alongside this is a config option which provides a way of rendering the provided + * component "offline", or outside the React render cycle. + */ +function createMotionComponent(_a) { + var preloadedFeatures = _a.preloadedFeatures, + createVisualElement = _a.createVisualElement, + projectionNodeConstructor = _a.projectionNodeConstructor, + useRender = _a.useRender, + useVisualState = _a.useVisualState, + Component = _a.Component; + preloadedFeatures && loadFeatures(preloadedFeatures); + function MotionComponent(props, externalRef) { + var layoutId = useLayoutId(props); + props = tslib.__assign(tslib.__assign({}, props), { + layoutId: layoutId + }); + /** + * If we're rendering in a static environment, we only visually update the component + * as a result of a React-rerender rather than interactions or animations. This + * means we don't need to load additional memory structures like VisualElement, + * or any gesture/animation features. + */ + var config = React.useContext(MotionConfigContext); + var features = null; + var context = useCreateMotionContext(props); + /** + * Create a unique projection ID for this component. If a new component is added + * during a layout animation we'll use this to query the DOM and hydrate its ref early, allowing + * us to measure it as soon as any layout effect flushes pending layout animations. + * + * Performance note: It'd be better not to have to search the DOM for these elements. + * For newly-entering components it could be enough to only correct treeScale, in which + * case we could mount in a scale-correction mode. This wouldn't be enough for + * shared element transitions however. Perhaps for those we could revert to a root node + * that gets forceRendered and layout animations are triggered on its layout effect. + */ + var projectionId = config.isStatic ? undefined : useProjectionId(); + /** + * + */ + var visualState = useVisualState(props, config.isStatic); + if (!config.isStatic && isBrowser) { + /** + * Create a VisualElement for this component. A VisualElement provides a common + * interface to renderer-specific APIs (ie DOM/Three.js etc) as well as + * providing a way of rendering to these APIs outside of the React render loop + * for more performant animations and interactions + */ + context.visualElement = useVisualElement(Component, visualState, tslib.__assign(tslib.__assign({}, config), props), createVisualElement); + useProjection(projectionId, props, context.visualElement, projectionNodeConstructor || featureDefinitions.projectionNodeConstructor); + /** + * Load Motion gesture and animation features. These are rendered as renderless + * components so each feature can optionally make use of React lifecycle methods. + */ + features = useFeatures(props, context.visualElement, preloadedFeatures); + } + /** + * The mount order and hierarchy is specific to ensure our element ref + * is hydrated by the time features fire their effects. + */ + return React__namespace.createElement(VisualElementHandler, { + visualElement: context.visualElement, + props: tslib.__assign(tslib.__assign({}, config), props) + }, features, React__namespace.createElement(MotionContext.Provider, { + value: context + }, useRender(Component, props, projectionId, useMotionRef(visualState, context.visualElement, externalRef), visualState, config.isStatic, context.visualElement))); + } + return React.forwardRef(MotionComponent); +} +function useLayoutId(_a) { + var _b; + var layoutId = _a.layoutId; + var layoutGroupId = (_b = React.useContext(LayoutGroupContext)) === null || _b === void 0 ? void 0 : _b.id; + return layoutGroupId && layoutId !== undefined ? layoutGroupId + "-" + layoutId : layoutId; +} + +/** + * Convert any React component into a `motion` component. The provided component + * **must** use `React.forwardRef` to the underlying DOM component you want to animate. + * + * ```jsx + * const Component = React.forwardRef((props, ref) => { + * return
+ * }) + * + * const MotionComponent = motion(Component) + * ``` + * + * @public + */ +function createMotionProxy(createConfig) { + function custom(Component, customMotionComponentConfig) { + if (customMotionComponentConfig === void 0) { + customMotionComponentConfig = {}; + } + return createMotionComponent(createConfig(Component, customMotionComponentConfig)); + } + if (typeof Proxy === "undefined") { + return custom; + } + /** + * A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc. + * Rather than generating them anew every render. + */ + var componentCache = new Map(); + return new Proxy(custom, { + /** + * Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc. + * The prop name is passed through as `key` and we can use that to generate a `motion` + * DOM component with that name. + */ + get: function (_target, key) { + /** + * If this element doesn't exist in the component cache, create it and cache. + */ + if (!componentCache.has(key)) { + componentCache.set(key, custom(key)); + } + return componentCache.get(key); + } + }); +} + +/** + * We keep these listed seperately as we use the lowercase tag names as part + * of the runtime bundle to detect SVG components + */ +var lowercaseSVGElements = ["animate", "circle", "defs", "desc", "ellipse", "g", "image", "line", "filter", "marker", "mask", "metadata", "path", "pattern", "polygon", "polyline", "rect", "stop", "svg", "switch", "symbol", "text", "tspan", "use", "view"]; +function isSVGComponent(Component) { + if ( + /** + * If it's not a string, it's a custom React component. Currently we only support + * HTML custom React components. + */ + typeof Component !== "string" || + /** + * If it contains a dash, the element is a custom HTML webcomponent. + */ + Component.includes("-")) { + return false; + } else if ( + /** + * If it's in our list of lowercase SVG tags, it's an SVG component + */ + lowercaseSVGElements.indexOf(Component) > -1 || + /** + * If it contains a capital letter, it's an SVG component + */ + /[A-Z]/.test(Component)) { + return true; + } + return false; +} +var scaleCorrectors = {}; +function addScaleCorrector(correctors) { + Object.assign(scaleCorrectors, correctors); +} + +/** + * A list of all transformable axes. We'll use this list to generated a version + * of each axes for each transform. + */ +var transformAxes = ["", "X", "Y", "Z"]; +/** + * An ordered array of each transformable value. By default, transform values + * will be sorted to this order. + */ +var order = ["translate", "scale", "rotate", "skew"]; +/** + * Generate a list of every possible transform key. + */ +var transformProps = ["transformPerspective", "x", "y", "z"]; +order.forEach(function (operationKey) { + return transformAxes.forEach(function (axesKey) { + return transformProps.push(operationKey + axesKey); + }); +}); +/** + * A function to use with Array.sort to sort transform keys by their default order. + */ +function sortTransformProps(a, b) { + return transformProps.indexOf(a) - transformProps.indexOf(b); +} +/** + * A quick lookup for transform props. + */ +var transformPropSet = new Set(transformProps); +function isTransformProp(key) { + return transformPropSet.has(key); +} +/** + * A quick lookup for transform origin props + */ +var transformOriginProps = new Set(["originX", "originY", "originZ"]); +function isTransformOriginProp(key) { + return transformOriginProps.has(key); +} +function isForcedMotionValue(key, _a) { + var layout = _a.layout, + layoutId = _a.layoutId; + return isTransformProp(key) || isTransformOriginProp(key) || (layout || layoutId !== undefined) && (!!scaleCorrectors[key] || key === "opacity"); +} +var isMotionValue = function (value) { + return Boolean(value !== null && typeof value === "object" && value.getVelocity); +}; +var translateAlias = { + x: "translateX", + y: "translateY", + z: "translateZ", + transformPerspective: "perspective" +}; +/** + * Build a CSS transform style from individual x/y/scale etc properties. + * + * This outputs with a default order of transforms/scales/rotations, this can be customised by + * providing a transformTemplate function. + */ +function buildTransform(_a, _b, transformIsDefault, transformTemplate) { + var transform = _a.transform, + transformKeys = _a.transformKeys; + var _c = _b.enableHardwareAcceleration, + enableHardwareAcceleration = _c === void 0 ? true : _c, + _d = _b.allowTransformNone, + allowTransformNone = _d === void 0 ? true : _d; + // The transform string we're going to build into. + var transformString = ""; + // Transform keys into their default order - this will determine the output order. + transformKeys.sort(sortTransformProps); + // Track whether the defined transform has a defined z so we don't add a + // second to enable hardware acceleration + var transformHasZ = false; + // Loop over each transform and build them into transformString + var numTransformKeys = transformKeys.length; + for (var i = 0; i < numTransformKeys; i++) { + var key = transformKeys[i]; + transformString += "".concat(translateAlias[key] || key, "(").concat(transform[key], ") "); + if (key === "z") transformHasZ = true; + } + if (!transformHasZ && enableHardwareAcceleration) { + transformString += "translateZ(0)"; + } else { + transformString = transformString.trim(); + } + // If we have a custom `transform` template, pass our transform values and + // generated transformString to that before returning + if (transformTemplate) { + transformString = transformTemplate(transform, transformIsDefault ? "" : transformString); + } else if (allowTransformNone && transformIsDefault) { + transformString = "none"; + } + return transformString; +} +/** + * Build a transformOrigin style. Uses the same defaults as the browser for + * undefined origins. + */ +function buildTransformOrigin(_a) { + var _b = _a.originX, + originX = _b === void 0 ? "50%" : _b, + _c = _a.originY, + originY = _c === void 0 ? "50%" : _c, + _d = _a.originZ, + originZ = _d === void 0 ? 0 : _d; + return "".concat(originX, " ").concat(originY, " ").concat(originZ); +} + +/** + * Returns true if the provided key is a CSS variable + */ +function isCSSVariable$1(key) { + return key.startsWith("--"); +} + +/** + * Provided a value and a ValueType, returns the value as that value type. + */ +var getValueAsType = function (value, type) { + return type && typeof value === "number" ? type.transform(value) : value; +}; +var int = tslib.__assign(tslib.__assign({}, styleValueTypes.number), { + transform: Math.round +}); +var numberValueTypes = { + // Border props + borderWidth: styleValueTypes.px, + borderTopWidth: styleValueTypes.px, + borderRightWidth: styleValueTypes.px, + borderBottomWidth: styleValueTypes.px, + borderLeftWidth: styleValueTypes.px, + borderRadius: styleValueTypes.px, + radius: styleValueTypes.px, + borderTopLeftRadius: styleValueTypes.px, + borderTopRightRadius: styleValueTypes.px, + borderBottomRightRadius: styleValueTypes.px, + borderBottomLeftRadius: styleValueTypes.px, + // Positioning props + width: styleValueTypes.px, + maxWidth: styleValueTypes.px, + height: styleValueTypes.px, + maxHeight: styleValueTypes.px, + size: styleValueTypes.px, + top: styleValueTypes.px, + right: styleValueTypes.px, + bottom: styleValueTypes.px, + left: styleValueTypes.px, + // Spacing props + padding: styleValueTypes.px, + paddingTop: styleValueTypes.px, + paddingRight: styleValueTypes.px, + paddingBottom: styleValueTypes.px, + paddingLeft: styleValueTypes.px, + margin: styleValueTypes.px, + marginTop: styleValueTypes.px, + marginRight: styleValueTypes.px, + marginBottom: styleValueTypes.px, + marginLeft: styleValueTypes.px, + // Transform props + rotate: styleValueTypes.degrees, + rotateX: styleValueTypes.degrees, + rotateY: styleValueTypes.degrees, + rotateZ: styleValueTypes.degrees, + scale: styleValueTypes.scale, + scaleX: styleValueTypes.scale, + scaleY: styleValueTypes.scale, + scaleZ: styleValueTypes.scale, + skew: styleValueTypes.degrees, + skewX: styleValueTypes.degrees, + skewY: styleValueTypes.degrees, + distance: styleValueTypes.px, + translateX: styleValueTypes.px, + translateY: styleValueTypes.px, + translateZ: styleValueTypes.px, + x: styleValueTypes.px, + y: styleValueTypes.px, + z: styleValueTypes.px, + perspective: styleValueTypes.px, + transformPerspective: styleValueTypes.px, + opacity: styleValueTypes.alpha, + originX: styleValueTypes.progressPercentage, + originY: styleValueTypes.progressPercentage, + originZ: styleValueTypes.px, + // Misc + zIndex: int, + // SVG + fillOpacity: styleValueTypes.alpha, + strokeOpacity: styleValueTypes.alpha, + numOctaves: int +}; +function buildHTMLStyles(state, latestValues, options, transformTemplate) { + var _a; + var style = state.style, + vars = state.vars, + transform = state.transform, + transformKeys = state.transformKeys, + transformOrigin = state.transformOrigin; + // Empty the transformKeys array. As we're throwing out refs to its items + // this might not be as cheap as suspected. Maybe using the array as a buffer + // with a manual incrementation would be better. + transformKeys.length = 0; + // Track whether we encounter any transform or transformOrigin values. + var hasTransform = false; + var hasTransformOrigin = false; + // Does the calculated transform essentially equal "none"? + var transformIsNone = true; + /** + * Loop over all our latest animated values and decide whether to handle them + * as a style or CSS variable. + * + * Transforms and transform origins are kept seperately for further processing. + */ + for (var key in latestValues) { + var value = latestValues[key]; + /** + * If this is a CSS variable we don't do any further processing. + */ + if (isCSSVariable$1(key)) { + vars[key] = value; + continue; + } + // Convert the value to its default value type, ie 0 -> "0px" + var valueType = numberValueTypes[key]; + var valueAsType = getValueAsType(value, valueType); + if (isTransformProp(key)) { + // If this is a transform, flag to enable further transform processing + hasTransform = true; + transform[key] = valueAsType; + transformKeys.push(key); + // If we already know we have a non-default transform, early return + if (!transformIsNone) continue; + // Otherwise check to see if this is a default transform + if (value !== ((_a = valueType.default) !== null && _a !== void 0 ? _a : 0)) transformIsNone = false; + } else if (isTransformOriginProp(key)) { + transformOrigin[key] = valueAsType; + // If this is a transform origin, flag and enable further transform-origin processing + hasTransformOrigin = true; + } else { + style[key] = valueAsType; + } + } + if (hasTransform) { + style.transform = buildTransform(state, options, transformIsNone, transformTemplate); + } else if (transformTemplate) { + style.transform = transformTemplate({}, ""); + } else if (!latestValues.transform && style.transform) { + style.transform = "none"; + } + if (hasTransformOrigin) { + style.transformOrigin = buildTransformOrigin(transformOrigin); + } +} +var createHtmlRenderState = function () { + return { + style: {}, + transform: {}, + transformKeys: [], + transformOrigin: {}, + vars: {} + }; +}; +function copyRawValuesOnly(target, source, props) { + for (var key in source) { + if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) { + target[key] = source[key]; + } + } +} +function useInitialMotionValues(_a, visualState, isStatic) { + var transformTemplate = _a.transformTemplate; + return React.useMemo(function () { + var state = createHtmlRenderState(); + buildHTMLStyles(state, visualState, { + enableHardwareAcceleration: !isStatic + }, transformTemplate); + var vars = state.vars, + style = state.style; + return tslib.__assign(tslib.__assign({}, vars), style); + }, [visualState]); +} +function useStyle(props, visualState, isStatic) { + var styleProp = props.style || {}; + var style = {}; + /** + * Copy non-Motion Values straight into style + */ + copyRawValuesOnly(style, styleProp, props); + Object.assign(style, useInitialMotionValues(props, visualState, isStatic)); + if (props.transformValues) { + style = props.transformValues(style); + } + return style; +} +function useHTMLProps(props, visualState, isStatic) { + // The `any` isn't ideal but it is the type of createElement props argument + var htmlProps = {}; + var style = useStyle(props, visualState, isStatic); + if (Boolean(props.drag) && props.dragListener !== false) { + // Disable the ghost element when a user drags + htmlProps.draggable = false; + // Disable text selection + style.userSelect = style.WebkitUserSelect = style.WebkitTouchCallout = "none"; + // Disable scrolling on the draggable direction + style.touchAction = props.drag === true ? "none" : "pan-".concat(props.drag === "x" ? "y" : "x"); + } + htmlProps.style = style; + return htmlProps; +} + +/** + * A list of all valid MotionProps. + * + * @privateRemarks + * This doesn't throw if a `MotionProp` name is missing - it should. + */ +var validMotionProps = new Set(["initial", "animate", "exit", "style", "variants", "transition", "transformTemplate", "transformValues", "custom", "inherit", "layout", "layoutId", "layoutDependency", "onLayoutAnimationStart", "onLayoutAnimationComplete", "onLayoutMeasure", "onBeforeLayoutMeasure", "onAnimationStart", "onAnimationComplete", "onUpdate", "onDragStart", "onDrag", "onDragEnd", "onMeasureDragConstraints", "onDirectionLock", "onDragTransitionEnd", "drag", "dragControls", "dragListener", "dragConstraints", "dragDirectionLock", "dragSnapToOrigin", "_dragX", "_dragY", "dragElastic", "dragMomentum", "dragPropagation", "dragTransition", "whileDrag", "onPan", "onPanStart", "onPanEnd", "onPanSessionStart", "onTap", "onTapStart", "onTapCancel", "onHoverStart", "onHoverEnd", "whileFocus", "whileTap", "whileHover", "whileInView", "onViewportEnter", "onViewportLeave", "viewport", "layoutScroll"]); +/** + * Check whether a prop name is a valid `MotionProp` key. + * + * @param key - Name of the property to check + * @returns `true` is key is a valid `MotionProp`. + * + * @public + */ +function isValidMotionProp(key) { + return validMotionProps.has(key); +} +var shouldForward = function (key) { + return !isValidMotionProp(key); +}; +function loadExternalIsValidProp(isValidProp) { + if (!isValidProp) return; + // Explicitly filter our events + shouldForward = function (key) { + return key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(key); + }; +} +/** + * Emotion and Styled Components both allow users to pass through arbitrary props to their components + * to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which + * of these should be passed to the underlying DOM node. + * + * However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props + * as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props + * passed through the `custom` prop so it doesn't *need* the payload or computational overhead of + * `@emotion/is-prop-valid`, however to fix this problem we need to use it. + * + * By making it an optionalDependency we can offer this functionality only in the situations where it's + * actually required. + */ +try { + /** + * We attempt to import this package but require won't be defined in esm environments, in that case + * isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed + * in favour of explicit injection. + */ + loadExternalIsValidProp((__webpack_require__(/*! @emotion/is-prop-valid */ "../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.browser.esm.js")["default"])); +} catch (_a) { + // We don't need to actually do anything here - the fallback is the existing `isPropValid`. +} +function filterProps(props, isDom, forwardMotionProps) { + var filteredProps = {}; + for (var key in props) { + if (shouldForward(key) || forwardMotionProps === true && isValidMotionProp(key) || !isDom && !isValidMotionProp(key) || + // If trying to use native HTML drag events, forward drag listeners + props["draggable"] && key.startsWith("onDrag")) { + filteredProps[key] = props[key]; + } + } + return filteredProps; +} +function calcOrigin$1(origin, offset, size) { + return typeof origin === "string" ? origin : styleValueTypes.px.transform(offset + size * origin); +} +/** + * The SVG transform origin defaults are different to CSS and is less intuitive, + * so we use the measured dimensions of the SVG to reconcile these. + */ +function calcSVGTransformOrigin(dimensions, originX, originY) { + var pxOriginX = calcOrigin$1(originX, dimensions.x, dimensions.width); + var pxOriginY = calcOrigin$1(originY, dimensions.y, dimensions.height); + return "".concat(pxOriginX, " ").concat(pxOriginY); +} +var dashKeys = { + offset: "stroke-dashoffset", + array: "stroke-dasharray" +}; +var camelKeys = { + offset: "strokeDashoffset", + array: "strokeDasharray" +}; +/** + * Build SVG path properties. Uses the path's measured length to convert + * our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset + * and stroke-dasharray attributes. + * + * This function is mutative to reduce per-frame GC. + */ +function buildSVGPath(attrs, length, spacing, offset, useDashCase) { + if (spacing === void 0) { + spacing = 1; + } + if (offset === void 0) { + offset = 0; + } + if (useDashCase === void 0) { + useDashCase = true; + } + // Normalise path length by setting SVG attribute pathLength to 1 + attrs.pathLength = 1; + // We use dash case when setting attributes directly to the DOM node and camel case + // when defining props on a React component. + var keys = useDashCase ? dashKeys : camelKeys; + // Build the dash offset + attrs[keys.offset] = styleValueTypes.px.transform(-offset); + // Build the dash array + var pathLength = styleValueTypes.px.transform(length); + var pathSpacing = styleValueTypes.px.transform(spacing); + attrs[keys.array] = "".concat(pathLength, " ").concat(pathSpacing); +} + +/** + * Build SVG visual attrbutes, like cx and style.transform + */ +function buildSVGAttrs(state, _a, options, transformTemplate) { + var attrX = _a.attrX, + attrY = _a.attrY, + originX = _a.originX, + originY = _a.originY, + pathLength = _a.pathLength, + _b = _a.pathSpacing, + pathSpacing = _b === void 0 ? 1 : _b, + _c = _a.pathOffset, + pathOffset = _c === void 0 ? 0 : _c, + // This is object creation, which we try to avoid per-frame. + latest = tslib.__rest(_a, ["attrX", "attrY", "originX", "originY", "pathLength", "pathSpacing", "pathOffset"]); + buildHTMLStyles(state, latest, options, transformTemplate); + state.attrs = state.style; + state.style = {}; + var attrs = state.attrs, + style = state.style, + dimensions = state.dimensions; + /** + * However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs + * and copy it into style. + */ + if (attrs.transform) { + if (dimensions) style.transform = attrs.transform; + delete attrs.transform; + } + // Parse transformOrigin + if (dimensions && (originX !== undefined || originY !== undefined || style.transform)) { + style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5); + } + // Treat x/y not as shortcuts but as actual attributes + if (attrX !== undefined) attrs.x = attrX; + if (attrY !== undefined) attrs.y = attrY; + // Build SVG path if one has been defined + if (pathLength !== undefined) { + buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false); + } +} +var createSvgRenderState = function () { + return tslib.__assign(tslib.__assign({}, createHtmlRenderState()), { + attrs: {} + }); +}; +function useSVGProps(props, visualState) { + var visualProps = React.useMemo(function () { + var state = createSvgRenderState(); + buildSVGAttrs(state, visualState, { + enableHardwareAcceleration: false + }, props.transformTemplate); + return tslib.__assign(tslib.__assign({}, state.attrs), { + style: tslib.__assign({}, state.style) + }); + }, [visualState]); + if (props.style) { + var rawStyles = {}; + copyRawValuesOnly(rawStyles, props.style, props); + visualProps.style = tslib.__assign(tslib.__assign({}, rawStyles), visualProps.style); + } + return visualProps; +} +function createUseRender(forwardMotionProps) { + if (forwardMotionProps === void 0) { + forwardMotionProps = false; + } + var useRender = function (Component, props, projectionId, ref, _a, isStatic) { + var latestValues = _a.latestValues; + var useVisualProps = isSVGComponent(Component) ? useSVGProps : useHTMLProps; + var visualProps = useVisualProps(props, latestValues, isStatic); + var filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps); + var elementProps = tslib.__assign(tslib.__assign(tslib.__assign({}, filteredProps), visualProps), { + ref: ref + }); + if (projectionId) { + elementProps["data-projection-id"] = projectionId; + } + return React.createElement(Component, elementProps); + }; + return useRender; +} +var CAMEL_CASE_PATTERN = /([a-z])([A-Z])/g; +var REPLACE_TEMPLATE = "$1-$2"; +/** + * Convert camelCase to dash-case properties. + */ +var camelToDash = function (str) { + return str.replace(CAMEL_CASE_PATTERN, REPLACE_TEMPLATE).toLowerCase(); +}; +function renderHTML(element, _a, styleProp, projection) { + var style = _a.style, + vars = _a.vars; + Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp)); + // Loop over any CSS variables and assign those. + for (var key in vars) { + element.style.setProperty(key, vars[key]); + } +} + +/** + * A set of attribute names that are always read/written as camel case. + */ +var camelCaseAttributes = new Set(["baseFrequency", "diffuseConstant", "kernelMatrix", "kernelUnitLength", "keySplines", "keyTimes", "limitingConeAngle", "markerHeight", "markerWidth", "numOctaves", "targetX", "targetY", "surfaceScale", "specularConstant", "specularExponent", "stdDeviation", "tableValues", "viewBox", "gradientTransform", "pathLength"]); +function renderSVG(element, renderState, _styleProp, projection) { + renderHTML(element, renderState, undefined, projection); + for (var key in renderState.attrs) { + element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]); + } +} +function scrapeMotionValuesFromProps$1(props) { + var style = props.style; + var newValues = {}; + for (var key in style) { + if (isMotionValue(style[key]) || isForcedMotionValue(key, props)) { + newValues[key] = style[key]; + } + } + return newValues; +} +function scrapeMotionValuesFromProps(props) { + var newValues = scrapeMotionValuesFromProps$1(props); + for (var key in props) { + if (isMotionValue(props[key])) { + var targetKey = key === "x" || key === "y" ? "attr" + key.toUpperCase() : key; + newValues[targetKey] = props[key]; + } + } + return newValues; +} +function isAnimationControls(v) { + return typeof v === "object" && typeof v.start === "function"; +} +var isKeyframesTarget = function (v) { + return Array.isArray(v); +}; +var isCustomValue = function (v) { + return Boolean(v && typeof v === "object" && v.mix && v.toValue); +}; +var resolveFinalValueInKeyframes = function (v) { + // TODO maybe throw if v.length - 1 is placeholder token? + return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v; +}; + +/** + * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself + * + * TODO: Remove and move to library + */ +function resolveMotionValue(value) { + var unwrappedValue = isMotionValue(value) ? value.get() : value; + return isCustomValue(unwrappedValue) ? unwrappedValue.toValue() : unwrappedValue; +} +function makeState(_a, props, context, presenceContext) { + var scrapeMotionValuesFromProps = _a.scrapeMotionValuesFromProps, + createRenderState = _a.createRenderState, + onMount = _a.onMount; + var state = { + latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps), + renderState: createRenderState() + }; + if (onMount) { + state.mount = function (instance) { + return onMount(props, instance, state); + }; + } + return state; +} +var makeUseVisualState = function (config) { + return function (props, isStatic) { + var context = React.useContext(MotionContext); + var presenceContext = React.useContext(PresenceContext); + return isStatic ? makeState(config, props, context, presenceContext) : useConstant(function () { + return makeState(config, props, context, presenceContext); + }); + }; +}; +function makeLatestValues(props, context, presenceContext, scrapeMotionValues) { + var values = {}; + var blockInitialAnimation = (presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.initial) === false; + var motionValues = scrapeMotionValues(props); + for (var key in motionValues) { + values[key] = resolveMotionValue(motionValues[key]); + } + var initial = props.initial, + animate = props.animate; + var isControllingVariants = checkIfControllingVariants(props); + var isVariantNode = checkIfVariantNode(props); + if (context && isVariantNode && !isControllingVariants && props.inherit !== false) { + initial !== null && initial !== void 0 ? initial : initial = context.initial; + animate !== null && animate !== void 0 ? animate : animate = context.animate; + } + var initialAnimationIsBlocked = blockInitialAnimation || initial === false; + var variantToSet = initialAnimationIsBlocked ? animate : initial; + if (variantToSet && typeof variantToSet !== "boolean" && !isAnimationControls(variantToSet)) { + var list = Array.isArray(variantToSet) ? variantToSet : [variantToSet]; + list.forEach(function (definition) { + var resolved = resolveVariantFromProps(props, definition); + if (!resolved) return; + var transitionEnd = resolved.transitionEnd; + resolved.transition; + var target = tslib.__rest(resolved, ["transitionEnd", "transition"]); + for (var key in target) { + var valueTarget = target[key]; + if (Array.isArray(valueTarget)) { + /** + * Take final keyframe if the initial animation is blocked because + * we want to initialise at the end of that blocked animation. + */ + var index = initialAnimationIsBlocked ? valueTarget.length - 1 : 0; + valueTarget = valueTarget[index]; + } + if (valueTarget !== null) { + values[key] = valueTarget; + } + } + for (var key in transitionEnd) values[key] = transitionEnd[key]; + }); + } + return values; +} +var svgMotionConfig = { + useVisualState: makeUseVisualState({ + scrapeMotionValuesFromProps: scrapeMotionValuesFromProps, + createRenderState: createSvgRenderState, + onMount: function (props, instance, _a) { + var renderState = _a.renderState, + latestValues = _a.latestValues; + try { + renderState.dimensions = typeof instance.getBBox === "function" ? instance.getBBox() : instance.getBoundingClientRect(); + } catch (e) { + // Most likely trying to measure an unrendered element under Firefox + renderState.dimensions = { + x: 0, + y: 0, + width: 0, + height: 0 + }; + } + buildSVGAttrs(renderState, latestValues, { + enableHardwareAcceleration: false + }, props.transformTemplate); + renderSVG(instance, renderState); + } + }) +}; +var htmlMotionConfig = { + useVisualState: makeUseVisualState({ + scrapeMotionValuesFromProps: scrapeMotionValuesFromProps$1, + createRenderState: createHtmlRenderState + }) +}; +function createDomMotionConfig(Component, _a, preloadedFeatures, createVisualElement, projectionNodeConstructor) { + var _b = _a.forwardMotionProps, + forwardMotionProps = _b === void 0 ? false : _b; + var baseConfig = isSVGComponent(Component) ? svgMotionConfig : htmlMotionConfig; + return tslib.__assign(tslib.__assign({}, baseConfig), { + preloadedFeatures: preloadedFeatures, + useRender: createUseRender(forwardMotionProps), + createVisualElement: createVisualElement, + projectionNodeConstructor: projectionNodeConstructor, + Component: Component + }); +} +exports.AnimationType = void 0; +(function (AnimationType) { + AnimationType["Animate"] = "animate"; + AnimationType["Hover"] = "whileHover"; + AnimationType["Tap"] = "whileTap"; + AnimationType["Drag"] = "whileDrag"; + AnimationType["Focus"] = "whileFocus"; + AnimationType["InView"] = "whileInView"; + AnimationType["Exit"] = "exit"; +})(exports.AnimationType || (exports.AnimationType = {})); +function addDomEvent(target, eventName, handler, options) { + if (options === void 0) { + options = { + passive: true + }; + } + target.addEventListener(eventName, handler, options); + return function () { + return target.removeEventListener(eventName, handler); + }; +} +/** + * Attaches an event listener directly to the provided DOM element. + * + * Bypassing React's event system can be desirable, for instance when attaching non-passive + * event handlers. + * + * ```jsx + * const ref = useRef(null) + * + * useDomEvent(ref, 'wheel', onWheel, { passive: false }) + * + * return
+ * ``` + * + * @param ref - React.RefObject that's been provided to the element you want to bind the listener to. + * @param eventName - Name of the event you want listen for. + * @param handler - Function to fire when receiving the event. + * @param options - Options to pass to `Event.addEventListener`. + * + * @public + */ +function useDomEvent(ref, eventName, handler, options) { + React.useEffect(function () { + var element = ref.current; + if (handler && element) { + return addDomEvent(element, eventName, handler, options); + } + }, [ref, eventName, handler, options]); +} + +/** + * + * @param props + * @param ref + * @internal + */ +function useFocusGesture(_a) { + var whileFocus = _a.whileFocus, + visualElement = _a.visualElement; + var onFocus = function () { + var _a; + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Focus, true); + }; + var onBlur = function () { + var _a; + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Focus, false); + }; + useDomEvent(visualElement, "focus", whileFocus ? onFocus : undefined); + useDomEvent(visualElement, "blur", whileFocus ? onBlur : undefined); +} +function isMouseEvent(event) { + // PointerEvent inherits from MouseEvent so we can't use a straight instanceof check. + if (typeof PointerEvent !== "undefined" && event instanceof PointerEvent) { + return !!(event.pointerType === "mouse"); + } + return event instanceof MouseEvent; +} +function isTouchEvent(event) { + var hasTouches = !!event.touches; + return hasTouches; +} + +/** + * Filters out events not attached to the primary pointer (currently left mouse button) + * @param eventHandler + */ +function filterPrimaryPointer(eventHandler) { + return function (event) { + var isMouseEvent = event instanceof MouseEvent; + var isPrimaryPointer = !isMouseEvent || isMouseEvent && event.button === 0; + if (isPrimaryPointer) { + eventHandler(event); + } + }; +} +var defaultPagePoint = { + pageX: 0, + pageY: 0 +}; +function pointFromTouch(e, pointType) { + if (pointType === void 0) { + pointType = "page"; + } + var primaryTouch = e.touches[0] || e.changedTouches[0]; + var point = primaryTouch || defaultPagePoint; + return { + x: point[pointType + "X"], + y: point[pointType + "Y"] + }; +} +function pointFromMouse(point, pointType) { + if (pointType === void 0) { + pointType = "page"; + } + return { + x: point[pointType + "X"], + y: point[pointType + "Y"] + }; +} +function extractEventInfo(event, pointType) { + if (pointType === void 0) { + pointType = "page"; + } + return { + point: isTouchEvent(event) ? pointFromTouch(event, pointType) : pointFromMouse(event, pointType) + }; +} +var wrapHandler = function (handler, shouldFilterPrimaryPointer) { + if (shouldFilterPrimaryPointer === void 0) { + shouldFilterPrimaryPointer = false; + } + var listener = function (event) { + return handler(event, extractEventInfo(event)); + }; + return shouldFilterPrimaryPointer ? filterPrimaryPointer(listener) : listener; +}; + +// We check for event support via functions in case they've been mocked by a testing suite. +var supportsPointerEvents = function () { + return isBrowser && window.onpointerdown === null; +}; +var supportsTouchEvents = function () { + return isBrowser && window.ontouchstart === null; +}; +var supportsMouseEvents = function () { + return isBrowser && window.onmousedown === null; +}; +var mouseEventNames = { + pointerdown: "mousedown", + pointermove: "mousemove", + pointerup: "mouseup", + pointercancel: "mousecancel", + pointerover: "mouseover", + pointerout: "mouseout", + pointerenter: "mouseenter", + pointerleave: "mouseleave" +}; +var touchEventNames = { + pointerdown: "touchstart", + pointermove: "touchmove", + pointerup: "touchend", + pointercancel: "touchcancel" +}; +function getPointerEventName(name) { + if (supportsPointerEvents()) { + return name; + } else if (supportsTouchEvents()) { + return touchEventNames[name]; + } else if (supportsMouseEvents()) { + return mouseEventNames[name]; + } + return name; +} +function addPointerEvent(target, eventName, handler, options) { + return addDomEvent(target, getPointerEventName(eventName), wrapHandler(handler, eventName === "pointerdown"), options); +} +function usePointerEvent(ref, eventName, handler, options) { + return useDomEvent(ref, getPointerEventName(eventName), handler && wrapHandler(handler, eventName === "pointerdown"), options); +} +function createLock(name) { + var lock = null; + return function () { + var openLock = function () { + lock = null; + }; + if (lock === null) { + lock = name; + return openLock; + } + return false; + }; +} +var globalHorizontalLock = createLock("dragHorizontal"); +var globalVerticalLock = createLock("dragVertical"); +function getGlobalLock(drag) { + var lock = false; + if (drag === "y") { + lock = globalVerticalLock(); + } else if (drag === "x") { + lock = globalHorizontalLock(); + } else { + var openHorizontal_1 = globalHorizontalLock(); + var openVertical_1 = globalVerticalLock(); + if (openHorizontal_1 && openVertical_1) { + lock = function () { + openHorizontal_1(); + openVertical_1(); + }; + } else { + // Release the locks because we don't use them + if (openHorizontal_1) openHorizontal_1(); + if (openVertical_1) openVertical_1(); + } + } + return lock; +} +function isDragActive() { + // Check the gesture lock - if we get it, it means no drag gesture is active + // and we can safely fire the tap gesture. + var openGestureLock = getGlobalLock(true); + if (!openGestureLock) return true; + openGestureLock(); + return false; +} +function createHoverEvent(visualElement, isActive, callback) { + return function (event, info) { + var _a; + if (!isMouseEvent(event) || isDragActive()) return; + /** + * Ensure we trigger animations before firing event callback + */ + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Hover, isActive); + callback === null || callback === void 0 ? void 0 : callback(event, info); + }; +} +function useHoverGesture(_a) { + var onHoverStart = _a.onHoverStart, + onHoverEnd = _a.onHoverEnd, + whileHover = _a.whileHover, + visualElement = _a.visualElement; + usePointerEvent(visualElement, "pointerenter", onHoverStart || whileHover ? createHoverEvent(visualElement, true, onHoverStart) : undefined, { + passive: !onHoverStart + }); + usePointerEvent(visualElement, "pointerleave", onHoverEnd || whileHover ? createHoverEvent(visualElement, false, onHoverEnd) : undefined, { + passive: !onHoverEnd + }); +} + +/** + * Recursively traverse up the tree to check whether the provided child node + * is the parent or a descendant of it. + * + * @param parent - Element to find + * @param child - Element to test against parent + */ +var isNodeOrChild = function (parent, child) { + if (!child) { + return false; + } else if (parent === child) { + return true; + } else { + return isNodeOrChild(parent, child.parentElement); + } +}; +function useUnmountEffect(callback) { + return React.useEffect(function () { + return function () { + return callback(); + }; + }, []); +} + +/** + * @param handlers - + * @internal + */ +function useTapGesture(_a) { + var onTap = _a.onTap, + onTapStart = _a.onTapStart, + onTapCancel = _a.onTapCancel, + whileTap = _a.whileTap, + visualElement = _a.visualElement; + var hasPressListeners = onTap || onTapStart || onTapCancel || whileTap; + var isPressing = React.useRef(false); + var cancelPointerEndListeners = React.useRef(null); + /** + * Only set listener to passive if there are no external listeners. + */ + var eventOptions = { + passive: !(onTapStart || onTap || onTapCancel || onPointerDown) + }; + function removePointerEndListener() { + var _a; + (_a = cancelPointerEndListeners.current) === null || _a === void 0 ? void 0 : _a.call(cancelPointerEndListeners); + cancelPointerEndListeners.current = null; + } + function checkPointerEnd() { + var _a; + removePointerEndListener(); + isPressing.current = false; + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Tap, false); + return !isDragActive(); + } + function onPointerUp(event, info) { + if (!checkPointerEnd()) return; + /** + * We only count this as a tap gesture if the event.target is the same + * as, or a child of, this component's element + */ + !isNodeOrChild(visualElement.getInstance(), event.target) ? onTapCancel === null || onTapCancel === void 0 ? void 0 : onTapCancel(event, info) : onTap === null || onTap === void 0 ? void 0 : onTap(event, info); + } + function onPointerCancel(event, info) { + if (!checkPointerEnd()) return; + onTapCancel === null || onTapCancel === void 0 ? void 0 : onTapCancel(event, info); + } + function onPointerDown(event, info) { + var _a; + removePointerEndListener(); + if (isPressing.current) return; + isPressing.current = true; + cancelPointerEndListeners.current = popmotion.pipe(addPointerEvent(window, "pointerup", onPointerUp, eventOptions), addPointerEvent(window, "pointercancel", onPointerCancel, eventOptions)); + /** + * Ensure we trigger animations before firing event callback + */ + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Tap, true); + onTapStart === null || onTapStart === void 0 ? void 0 : onTapStart(event, info); + } + usePointerEvent(visualElement, "pointerdown", hasPressListeners ? onPointerDown : undefined, eventOptions); + useUnmountEffect(removePointerEndListener); +} +var warned = new Set(); +function warnOnce(condition, message, element) { + if (condition || warned.has(message)) return; + console.warn(message); + if (element) console.warn(element); + warned.add(message); +} + +/** + * Map an IntersectionHandler callback to an element. We only ever make one handler for one + * element, so even though these handlers might all be triggered by different + * observers, we can keep them in the same map. + */ +var observerCallbacks = new WeakMap(); +/** + * Multiple observers can be created for multiple element/document roots. Each with + * different settings. So here we store dictionaries of observers to each root, + * using serialised settings (threshold/margin) as lookup keys. + */ +var observers = new WeakMap(); +var fireObserverCallback = function (entry) { + var _a; + (_a = observerCallbacks.get(entry.target)) === null || _a === void 0 ? void 0 : _a(entry); +}; +var fireAllObserverCallbacks = function (entries) { + entries.forEach(fireObserverCallback); +}; +function initIntersectionObserver(_a) { + var root = _a.root, + options = tslib.__rest(_a, ["root"]); + var lookupRoot = root || document; + /** + * If we don't have an observer lookup map for this root, create one. + */ + if (!observers.has(lookupRoot)) { + observers.set(lookupRoot, {}); + } + var rootObservers = observers.get(lookupRoot); + var key = JSON.stringify(options); + /** + * If we don't have an observer for this combination of root and settings, + * create one. + */ + if (!rootObservers[key]) { + rootObservers[key] = new IntersectionObserver(fireAllObserverCallbacks, tslib.__assign({ + root: root + }, options)); + } + return rootObservers[key]; +} +function observeIntersection(element, options, callback) { + var rootInteresectionObserver = initIntersectionObserver(options); + observerCallbacks.set(element, callback); + rootInteresectionObserver.observe(element); + return function () { + observerCallbacks.delete(element); + rootInteresectionObserver.unobserve(element); + }; +} +function useViewport(_a) { + var visualElement = _a.visualElement, + whileInView = _a.whileInView, + onViewportEnter = _a.onViewportEnter, + onViewportLeave = _a.onViewportLeave, + _b = _a.viewport, + viewport = _b === void 0 ? {} : _b; + var state = React.useRef({ + hasEnteredView: false, + isInView: false + }); + var shouldObserve = Boolean(whileInView || onViewportEnter || onViewportLeave); + if (viewport.once && state.current.hasEnteredView) shouldObserve = false; + var useObserver = typeof IntersectionObserver === "undefined" ? useMissingIntersectionObserver : useIntersectionObserver; + useObserver(shouldObserve, state.current, visualElement, viewport); +} +var thresholdNames = { + some: 0, + all: 1 +}; +function useIntersectionObserver(shouldObserve, state, visualElement, _a) { + var root = _a.root, + rootMargin = _a.margin, + _b = _a.amount, + amount = _b === void 0 ? "some" : _b, + once = _a.once; + React.useEffect(function () { + if (!shouldObserve) return; + var options = { + root: root === null || root === void 0 ? void 0 : root.current, + rootMargin: rootMargin, + threshold: typeof amount === "number" ? amount : thresholdNames[amount] + }; + var intersectionCallback = function (entry) { + var _a; + var isIntersecting = entry.isIntersecting; + /** + * If there's been no change in the viewport state, early return. + */ + if (state.isInView === isIntersecting) return; + state.isInView = isIntersecting; + /** + * Handle hasEnteredView. If this is only meant to run once, and + * element isn't visible, early return. Otherwise set hasEnteredView to true. + */ + if (once && !isIntersecting && state.hasEnteredView) { + return; + } else if (isIntersecting) { + state.hasEnteredView = true; + } + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.InView, isIntersecting); + /** + * Use the latest committed props rather than the ones in scope + * when this observer is created + */ + var props = visualElement.getProps(); + var callback = isIntersecting ? props.onViewportEnter : props.onViewportLeave; + callback === null || callback === void 0 ? void 0 : callback(entry); + }; + return observeIntersection(visualElement.getInstance(), options, intersectionCallback); + }, [shouldObserve, root, rootMargin, amount]); +} +/** + * If IntersectionObserver is missing, we activate inView and fire onViewportEnter + * on mount. This way, the page will be in the state the author expects users + * to see it in for everyone. + */ +function useMissingIntersectionObserver(shouldObserve, state, visualElement, _a) { + var _b = _a.fallback, + fallback = _b === void 0 ? true : _b; + React.useEffect(function () { + if (!shouldObserve || !fallback) return; + if (env !== "production") { + warnOnce(false, "IntersectionObserver not available on this device. whileInView animations will trigger on mount."); + } + /** + * Fire this in an rAF because, at this point, the animation state + * won't have flushed for the first time and there's certain logic in + * there that behaves differently on the initial animation. + * + * This hook should be quite rarely called so setting this in an rAF + * is preferred to changing the behaviour of the animation state. + */ + requestAnimationFrame(function () { + var _a; + state.hasEnteredView = true; + var onViewportEnter = visualElement.getProps().onViewportEnter; + onViewportEnter === null || onViewportEnter === void 0 ? void 0 : onViewportEnter(null); + (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.InView, true); + }); + }, [shouldObserve]); +} +var makeRenderlessComponent = function (hook) { + return function (props) { + hook(props); + return null; + }; +}; +var gestureAnimations = { + inView: makeRenderlessComponent(useViewport), + tap: makeRenderlessComponent(useTapGesture), + focus: makeRenderlessComponent(useFocusGesture), + hover: makeRenderlessComponent(useHoverGesture) +}; +var counter = 0; +var incrementId = function () { + return counter++; +}; +var useId = function () { + return useConstant(incrementId); +}; +/** + * Ideally we'd use the following code to support React 18 optionally. + * But this fairly fails in Webpack (otherwise treeshaking wouldn't work at all). + * Need to come up with a different way of figuring this out. + */ +// export const useId = (React as any).useId +// ? (React as any).useId +// : () => useConstant(incrementId) + +/** + * When a component is the child of `AnimatePresence`, it can use `usePresence` + * to access information about whether it's still present in the React tree. + * + * ```jsx + * import { usePresence } from "framer-motion" + * + * export const Component = () => { + * const [isPresent, safeToRemove] = usePresence() + * + * useEffect(() => { + * !isPresent && setTimeout(safeToRemove, 1000) + * }, [isPresent]) + * + * return
+ * } + * ``` + * + * If `isPresent` is `false`, it means that a component has been removed the tree, but + * `AnimatePresence` won't really remove it until `safeToRemove` has been called. + * + * @public + */ +function usePresence() { + var context = React.useContext(PresenceContext); + if (context === null) return [true, null]; + var isPresent = context.isPresent, + onExitComplete = context.onExitComplete, + register = context.register; + // It's safe to call the following hooks conditionally (after an early return) because the context will always + // either be null or non-null for the lifespan of the component. + // Replace with useId when released in React + var id = useId(); + React.useEffect(function () { + return register(id); + }, []); + var safeToRemove = function () { + return onExitComplete === null || onExitComplete === void 0 ? void 0 : onExitComplete(id); + }; + return !isPresent && onExitComplete ? [false, safeToRemove] : [true]; +} +/** + * Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present. + * There is no `safeToRemove` function. + * + * ```jsx + * import { useIsPresent } from "framer-motion" + * + * export const Component = () => { + * const isPresent = useIsPresent() + * + * useEffect(() => { + * !isPresent && console.log("I've been removed!") + * }, [isPresent]) + * + * return
+ * } + * ``` + * + * @public + */ +function useIsPresent() { + return isPresent(React.useContext(PresenceContext)); +} +function isPresent(context) { + return context === null ? true : context.isPresent; +} +function shallowCompare(next, prev) { + if (!Array.isArray(prev)) return false; + var prevLength = prev.length; + if (prevLength !== next.length) return false; + for (var i = 0; i < prevLength; i++) { + if (prev[i] !== next[i]) return false; + } + return true; +} + +/** + * Converts seconds to milliseconds + * + * @param seconds - Time in seconds. + * @return milliseconds - Converted time in milliseconds. + */ +var secondsToMilliseconds = function (seconds) { + return seconds * 1000; +}; +var easingLookup = { + linear: popmotion.linear, + easeIn: popmotion.easeIn, + easeInOut: popmotion.easeInOut, + easeOut: popmotion.easeOut, + circIn: popmotion.circIn, + circInOut: popmotion.circInOut, + circOut: popmotion.circOut, + backIn: popmotion.backIn, + backInOut: popmotion.backInOut, + backOut: popmotion.backOut, + anticipate: popmotion.anticipate, + bounceIn: popmotion.bounceIn, + bounceInOut: popmotion.bounceInOut, + bounceOut: popmotion.bounceOut +}; +var easingDefinitionToFunction = function (definition) { + if (Array.isArray(definition)) { + // If cubic bezier definition, create bezier curve + heyListen.invariant(definition.length === 4, "Cubic bezier arrays must contain four numerical values."); + var _a = tslib.__read(definition, 4), + x1 = _a[0], + y1 = _a[1], + x2 = _a[2], + y2 = _a[3]; + return popmotion.cubicBezier(x1, y1, x2, y2); + } else if (typeof definition === "string") { + // Else lookup from table + heyListen.invariant(easingLookup[definition] !== undefined, "Invalid easing type '".concat(definition, "'")); + return easingLookup[definition]; + } + return definition; +}; +var isEasingArray = function (ease) { + return Array.isArray(ease) && typeof ease[0] !== "number"; +}; + +/** + * Check if a value is animatable. Examples: + * + * ✅: 100, "100px", "#fff" + * ❌: "block", "url(2.jpg)" + * @param value + * + * @internal + */ +var isAnimatable = function (key, value) { + // If the list of keys tat might be non-animatable grows, replace with Set + if (key === "zIndex") return false; + // If it's a number or a keyframes array, we can animate it. We might at some point + // need to do a deep isAnimatable check of keyframes, or let Popmotion handle this, + // but for now lets leave it like this for performance reasons + if (typeof value === "number" || Array.isArray(value)) return true; + if (typeof value === "string" && + // It's animatable if we have a string + styleValueTypes.complex.test(value) && + // And it contains numbers and/or colors + !value.startsWith("url(") // Unless it starts with "url(" + ) { + return true; + } + return false; +}; +var underDampedSpring = function () { + return { + type: "spring", + stiffness: 500, + damping: 25, + restSpeed: 10 + }; +}; +var criticallyDampedSpring = function (to) { + return { + type: "spring", + stiffness: 550, + damping: to === 0 ? 2 * Math.sqrt(550) : 30, + restSpeed: 10 + }; +}; +var linearTween = function () { + return { + type: "keyframes", + ease: "linear", + duration: 0.3 + }; +}; +var keyframes = function (values) { + return { + type: "keyframes", + duration: 0.8, + values: values + }; +}; +var defaultTransitions = { + x: underDampedSpring, + y: underDampedSpring, + z: underDampedSpring, + rotate: underDampedSpring, + rotateX: underDampedSpring, + rotateY: underDampedSpring, + rotateZ: underDampedSpring, + scaleX: criticallyDampedSpring, + scaleY: criticallyDampedSpring, + scale: criticallyDampedSpring, + opacity: linearTween, + backgroundColor: linearTween, + color: linearTween, + default: criticallyDampedSpring +}; +var getDefaultTransition = function (valueKey, to) { + var transitionFactory; + if (isKeyframesTarget(to)) { + transitionFactory = keyframes; + } else { + transitionFactory = defaultTransitions[valueKey] || defaultTransitions.default; + } + return tslib.__assign({ + to: to + }, transitionFactory(to)); +}; + +/** + * A map of default value types for common values + */ +var defaultValueTypes = tslib.__assign(tslib.__assign({}, numberValueTypes), { + // Color props + color: styleValueTypes.color, + backgroundColor: styleValueTypes.color, + outlineColor: styleValueTypes.color, + fill: styleValueTypes.color, + stroke: styleValueTypes.color, + // Border props + borderColor: styleValueTypes.color, + borderTopColor: styleValueTypes.color, + borderRightColor: styleValueTypes.color, + borderBottomColor: styleValueTypes.color, + borderLeftColor: styleValueTypes.color, + filter: styleValueTypes.filter, + WebkitFilter: styleValueTypes.filter +}); +/** + * Gets the default ValueType for the provided value key + */ +var getDefaultValueType = function (key) { + return defaultValueTypes[key]; +}; +function getAnimatableNone(key, value) { + var _a; + var defaultValueType = getDefaultValueType(key); + if (defaultValueType !== styleValueTypes.filter) defaultValueType = styleValueTypes.complex; + // If value is not recognised as animatable, ie "none", create an animatable version origin based on the target + return (_a = defaultValueType.getAnimatableNone) === null || _a === void 0 ? void 0 : _a.call(defaultValueType, value); +} +var instantAnimationState = { + current: false +}; + +/** + * Decide whether a transition is defined on a given Transition. + * This filters out orchestration options and returns true + * if any options are left. + */ +function isTransitionDefined(_a) { + _a.when; + _a.delay; + _a.delayChildren; + _a.staggerChildren; + _a.staggerDirection; + _a.repeat; + _a.repeatType; + _a.repeatDelay; + _a.from; + var transition = tslib.__rest(_a, ["when", "delay", "delayChildren", "staggerChildren", "staggerDirection", "repeat", "repeatType", "repeatDelay", "from"]); + return !!Object.keys(transition).length; +} +var legacyRepeatWarning = false; +/** + * Convert Framer Motion's Transition type into Popmotion-compatible options. + */ +function convertTransitionToAnimationOptions(_a) { + var ease = _a.ease, + times = _a.times, + yoyo = _a.yoyo, + flip = _a.flip, + loop = _a.loop, + transition = tslib.__rest(_a, ["ease", "times", "yoyo", "flip", "loop"]); + var options = tslib.__assign({}, transition); + if (times) options["offset"] = times; + /** + * Convert any existing durations from seconds to milliseconds + */ + if (transition.duration) options["duration"] = secondsToMilliseconds(transition.duration); + if (transition.repeatDelay) options.repeatDelay = secondsToMilliseconds(transition.repeatDelay); + /** + * Map easing names to Popmotion's easing functions + */ + if (ease) { + options["ease"] = isEasingArray(ease) ? ease.map(easingDefinitionToFunction) : easingDefinitionToFunction(ease); + } + /** + * Support legacy transition API + */ + if (transition.type === "tween") options.type = "keyframes"; + /** + * TODO: These options are officially removed from the API. + */ + if (yoyo || loop || flip) { + heyListen.warning(!legacyRepeatWarning, "yoyo, loop and flip have been removed from the API. Replace with repeat and repeatType options."); + legacyRepeatWarning = true; + if (yoyo) { + options.repeatType = "reverse"; + } else if (loop) { + options.repeatType = "loop"; + } else if (flip) { + options.repeatType = "mirror"; + } + options.repeat = loop || yoyo || flip || transition.repeat; + } + /** + * TODO: Popmotion 9 has the ability to automatically detect whether to use + * a keyframes or spring animation, but does so by detecting velocity and other spring options. + * It'd be good to introduce a similar thing here. + */ + if (transition.type !== "spring") options.type = "keyframes"; + return options; +} +/** + * Get the delay for a value by checking Transition with decreasing specificity. + */ +function getDelayFromTransition(transition, key) { + var _a, _b; + var valueTransition = getValueTransition(transition, key) || {}; + return (_b = (_a = valueTransition.delay) !== null && _a !== void 0 ? _a : transition.delay) !== null && _b !== void 0 ? _b : 0; +} +function hydrateKeyframes(options) { + if (Array.isArray(options.to) && options.to[0] === null) { + options.to = tslib.__spreadArray([], tslib.__read(options.to), false); + options.to[0] = options.from; + } + return options; +} +function getPopmotionAnimationOptions(transition, options, key) { + var _a; + if (Array.isArray(options.to)) { + (_a = transition.duration) !== null && _a !== void 0 ? _a : transition.duration = 0.8; + } + hydrateKeyframes(options); + /** + * Get a default transition if none is determined to be defined. + */ + if (!isTransitionDefined(transition)) { + transition = tslib.__assign(tslib.__assign({}, transition), getDefaultTransition(key, options.to)); + } + return tslib.__assign(tslib.__assign({}, options), convertTransitionToAnimationOptions(transition)); +} +/** + * + */ +function getAnimation(key, value, target, transition, onComplete) { + var _a; + var valueTransition = getValueTransition(transition, key); + var origin = (_a = valueTransition.from) !== null && _a !== void 0 ? _a : value.get(); + var isTargetAnimatable = isAnimatable(key, target); + if (origin === "none" && isTargetAnimatable && typeof target === "string") { + /** + * If we're trying to animate from "none", try and get an animatable version + * of the target. This could be improved to work both ways. + */ + origin = getAnimatableNone(key, target); + } else if (isZero(origin) && typeof target === "string") { + origin = getZeroUnit(target); + } else if (!Array.isArray(target) && isZero(target) && typeof origin === "string") { + target = getZeroUnit(origin); + } + var isOriginAnimatable = isAnimatable(key, origin); + heyListen.warning(isOriginAnimatable === isTargetAnimatable, "You are trying to animate ".concat(key, " from \"").concat(origin, "\" to \"").concat(target, "\". ").concat(origin, " is not an animatable value - to enable this animation set ").concat(origin, " to a value animatable to ").concat(target, " via the `style` property.")); + function start() { + var options = { + from: origin, + to: target, + velocity: value.getVelocity(), + onComplete: onComplete, + onUpdate: function (v) { + return value.set(v); + } + }; + return valueTransition.type === "inertia" || valueTransition.type === "decay" ? popmotion.inertia(tslib.__assign(tslib.__assign({}, options), valueTransition)) : popmotion.animate(tslib.__assign(tslib.__assign({}, getPopmotionAnimationOptions(valueTransition, options, key)), { + onUpdate: function (v) { + var _a; + options.onUpdate(v); + (_a = valueTransition.onUpdate) === null || _a === void 0 ? void 0 : _a.call(valueTransition, v); + }, + onComplete: function () { + var _a; + options.onComplete(); + (_a = valueTransition.onComplete) === null || _a === void 0 ? void 0 : _a.call(valueTransition); + } + })); + } + function set() { + var _a, _b; + var finalTarget = resolveFinalValueInKeyframes(target); + value.set(finalTarget); + onComplete(); + (_a = valueTransition === null || valueTransition === void 0 ? void 0 : valueTransition.onUpdate) === null || _a === void 0 ? void 0 : _a.call(valueTransition, finalTarget); + (_b = valueTransition === null || valueTransition === void 0 ? void 0 : valueTransition.onComplete) === null || _b === void 0 ? void 0 : _b.call(valueTransition); + return { + stop: function () {} + }; + } + return !isOriginAnimatable || !isTargetAnimatable || valueTransition.type === false ? set : start; +} +function isZero(value) { + return value === 0 || typeof value === "string" && parseFloat(value) === 0 && value.indexOf(" ") === -1; +} +function getZeroUnit(potentialUnitType) { + return typeof potentialUnitType === "number" ? 0 : getAnimatableNone("", potentialUnitType); +} +function getValueTransition(transition, key) { + return transition[key] || transition["default"] || transition; +} +/** + * Start animation on a MotionValue. This function is an interface between + * Framer Motion and Popmotion + */ +function startAnimation(key, value, target, transition) { + if (transition === void 0) { + transition = {}; + } + if (instantAnimationState.current) { + transition = { + type: false + }; + } + return value.start(function (onComplete) { + var delayTimer; + var controls; + var animation = getAnimation(key, value, target, transition, onComplete); + var delay = getDelayFromTransition(transition, key); + var start = function () { + return controls = animation(); + }; + if (delay) { + delayTimer = window.setTimeout(start, secondsToMilliseconds(delay)); + } else { + start(); + } + return function () { + clearTimeout(delayTimer); + controls === null || controls === void 0 ? void 0 : controls.stop(); + }; + }); +} + +/** + * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1" + */ +var isNumericalString = function (v) { + return /^\-?\d*\.?\d+$/.test(v); +}; + +/** + * Check if the value is a zero value string like "0px" or "0%" + */ +var isZeroValueString = function (v) { + return /^0[^.\s]+$/.test(v); +}; +function addUniqueItem(arr, item) { + arr.indexOf(item) === -1 && arr.push(item); +} +function removeItem(arr, item) { + var index = arr.indexOf(item); + index > -1 && arr.splice(index, 1); +} +// Adapted from array-move +function moveItem(_a, fromIndex, toIndex) { + var _b = tslib.__read(_a), + arr = _b.slice(0); + var startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex; + if (startIndex >= 0 && startIndex < arr.length) { + var endIndex = toIndex < 0 ? arr.length + toIndex : toIndex; + var _c = tslib.__read(arr.splice(fromIndex, 1), 1), + item = _c[0]; + arr.splice(endIndex, 0, item); + } + return arr; +} +var SubscriptionManager = /** @class */function () { + function SubscriptionManager() { + this.subscriptions = []; + } + SubscriptionManager.prototype.add = function (handler) { + var _this = this; + addUniqueItem(this.subscriptions, handler); + return function () { + return removeItem(_this.subscriptions, handler); + }; + }; + SubscriptionManager.prototype.notify = function (a, b, c) { + var numSubscriptions = this.subscriptions.length; + if (!numSubscriptions) return; + if (numSubscriptions === 1) { + /** + * If there's only a single handler we can just call it without invoking a loop. + */ + this.subscriptions[0](a, b, c); + } else { + for (var i = 0; i < numSubscriptions; i++) { + /** + * Check whether the handler exists before firing as it's possible + * the subscriptions were modified during this loop running. + */ + var handler = this.subscriptions[i]; + handler && handler(a, b, c); + } + } + }; + SubscriptionManager.prototype.getSize = function () { + return this.subscriptions.length; + }; + SubscriptionManager.prototype.clear = function () { + this.subscriptions.length = 0; + }; + return SubscriptionManager; +}(); +var isFloat = function (value) { + return !isNaN(parseFloat(value)); +}; +/** + * `MotionValue` is used to track the state and velocity of motion values. + * + * @public + */ +var MotionValue = /** @class */function () { + /** + * @param init - The initiating value + * @param config - Optional configuration options + * + * - `transformer`: A function to transform incoming values with. + * + * @internal + */ + function MotionValue(init) { + var _this = this; + /** + * This will be replaced by the build step with the latest version number. + * When MotionValues are provided to motion components, warn if versions are mixed. + */ + this.version = "6.5.1"; + /** + * Duration, in milliseconds, since last updating frame. + * + * @internal + */ + this.timeDelta = 0; + /** + * Timestamp of the last time this `MotionValue` was updated. + * + * @internal + */ + this.lastUpdated = 0; + /** + * Functions to notify when the `MotionValue` updates. + * + * @internal + */ + this.updateSubscribers = new SubscriptionManager(); + /** + * Functions to notify when the velocity updates. + * + * @internal + */ + this.velocityUpdateSubscribers = new SubscriptionManager(); + /** + * Functions to notify when the `MotionValue` updates and `render` is set to `true`. + * + * @internal + */ + this.renderSubscribers = new SubscriptionManager(); + /** + * Tracks whether this value can output a velocity. Currently this is only true + * if the value is numerical, but we might be able to widen the scope here and support + * other value types. + * + * @internal + */ + this.canTrackVelocity = false; + this.updateAndNotify = function (v, render) { + if (render === void 0) { + render = true; + } + _this.prev = _this.current; + _this.current = v; + // Update timestamp + var _a = sync.getFrameData(), + delta = _a.delta, + timestamp = _a.timestamp; + if (_this.lastUpdated !== timestamp) { + _this.timeDelta = delta; + _this.lastUpdated = timestamp; + sync__default["default"].postRender(_this.scheduleVelocityCheck); + } + // Update update subscribers + if (_this.prev !== _this.current) { + _this.updateSubscribers.notify(_this.current); + } + // Update velocity subscribers + if (_this.velocityUpdateSubscribers.getSize()) { + _this.velocityUpdateSubscribers.notify(_this.getVelocity()); + } + // Update render subscribers + if (render) { + _this.renderSubscribers.notify(_this.current); + } + }; + /** + * Schedule a velocity check for the next frame. + * + * This is an instanced and bound function to prevent generating a new + * function once per frame. + * + * @internal + */ + this.scheduleVelocityCheck = function () { + return sync__default["default"].postRender(_this.velocityCheck); + }; + /** + * Updates `prev` with `current` if the value hasn't been updated this frame. + * This ensures velocity calculations return `0`. + * + * This is an instanced and bound function to prevent generating a new + * function once per frame. + * + * @internal + */ + this.velocityCheck = function (_a) { + var timestamp = _a.timestamp; + if (timestamp !== _this.lastUpdated) { + _this.prev = _this.current; + _this.velocityUpdateSubscribers.notify(_this.getVelocity()); + } + }; + this.hasAnimated = false; + this.prev = this.current = init; + this.canTrackVelocity = isFloat(this.current); + } + /** + * Adds a function that will be notified when the `MotionValue` is updated. + * + * It returns a function that, when called, will cancel the subscription. + * + * When calling `onChange` inside a React component, it should be wrapped with the + * `useEffect` hook. As it returns an unsubscribe function, this should be returned + * from the `useEffect` function to ensure you don't add duplicate subscribers.. + * + * ```jsx + * export const MyComponent = () => { + * const x = useMotionValue(0) + * const y = useMotionValue(0) + * const opacity = useMotionValue(1) + * + * useEffect(() => { + * function updateOpacity() { + * const maxXY = Math.max(x.get(), y.get()) + * const newOpacity = transform(maxXY, [0, 100], [1, 0]) + * opacity.set(newOpacity) + * } + * + * const unsubscribeX = x.onChange(updateOpacity) + * const unsubscribeY = y.onChange(updateOpacity) + * + * return () => { + * unsubscribeX() + * unsubscribeY() + * } + * }, []) + * + * return + * } + * ``` + * + * @privateRemarks + * + * We could look into a `useOnChange` hook if the above lifecycle management proves confusing. + * + * ```jsx + * useOnChange(x, () => {}) + * ``` + * + * @param subscriber - A function that receives the latest value. + * @returns A function that, when called, will cancel this subscription. + * + * @public + */ + MotionValue.prototype.onChange = function (subscription) { + return this.updateSubscribers.add(subscription); + }; + MotionValue.prototype.clearListeners = function () { + this.updateSubscribers.clear(); + }; + /** + * Adds a function that will be notified when the `MotionValue` requests a render. + * + * @param subscriber - A function that's provided the latest value. + * @returns A function that, when called, will cancel this subscription. + * + * @internal + */ + MotionValue.prototype.onRenderRequest = function (subscription) { + // Render immediately + subscription(this.get()); + return this.renderSubscribers.add(subscription); + }; + /** + * Attaches a passive effect to the `MotionValue`. + * + * @internal + */ + MotionValue.prototype.attach = function (passiveEffect) { + this.passiveEffect = passiveEffect; + }; + /** + * Sets the state of the `MotionValue`. + * + * @remarks + * + * ```jsx + * const x = useMotionValue(0) + * x.set(10) + * ``` + * + * @param latest - Latest value to set. + * @param render - Whether to notify render subscribers. Defaults to `true` + * + * @public + */ + MotionValue.prototype.set = function (v, render) { + if (render === void 0) { + render = true; + } + if (!render || !this.passiveEffect) { + this.updateAndNotify(v, render); + } else { + this.passiveEffect(v, this.updateAndNotify); + } + }; + /** + * Returns the latest state of `MotionValue` + * + * @returns - The latest state of `MotionValue` + * + * @public + */ + MotionValue.prototype.get = function () { + return this.current; + }; + /** + * @public + */ + MotionValue.prototype.getPrevious = function () { + return this.prev; + }; + /** + * Returns the latest velocity of `MotionValue` + * + * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical. + * + * @public + */ + MotionValue.prototype.getVelocity = function () { + // This could be isFloat(this.prev) && isFloat(this.current), but that would be wasteful + return this.canTrackVelocity ? + // These casts could be avoided if parseFloat would be typed better + popmotion.velocityPerSecond(parseFloat(this.current) - parseFloat(this.prev), this.timeDelta) : 0; + }; + /** + * Registers a new animation to control this `MotionValue`. Only one + * animation can drive a `MotionValue` at one time. + * + * ```jsx + * value.start() + * ``` + * + * @param animation - A function that starts the provided animation + * + * @internal + */ + MotionValue.prototype.start = function (animation) { + var _this = this; + this.stop(); + return new Promise(function (resolve) { + _this.hasAnimated = true; + _this.stopAnimation = animation(resolve); + }).then(function () { + return _this.clearAnimation(); + }); + }; + /** + * Stop the currently active animation. + * + * @public + */ + MotionValue.prototype.stop = function () { + if (this.stopAnimation) this.stopAnimation(); + this.clearAnimation(); + }; + /** + * Returns `true` if this value is currently animating. + * + * @public + */ + MotionValue.prototype.isAnimating = function () { + return !!this.stopAnimation; + }; + MotionValue.prototype.clearAnimation = function () { + this.stopAnimation = null; + }; + /** + * Destroy and clean up subscribers to this `MotionValue`. + * + * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically + * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually + * created a `MotionValue` via the `motionValue` function. + * + * @public + */ + MotionValue.prototype.destroy = function () { + this.updateSubscribers.clear(); + this.renderSubscribers.clear(); + this.stop(); + }; + return MotionValue; +}(); +function motionValue(init) { + return new MotionValue(init); +} + +/** + * Tests a provided value against a ValueType + */ +var testValueType = function (v) { + return function (type) { + return type.test(v); + }; +}; + +/** + * ValueType for "auto" + */ +var auto = { + test: function (v) { + return v === "auto"; + }, + parse: function (v) { + return v; + } +}; + +/** + * A list of value types commonly used for dimensions + */ +var dimensionValueTypes = [styleValueTypes.number, styleValueTypes.px, styleValueTypes.percent, styleValueTypes.degrees, styleValueTypes.vw, styleValueTypes.vh, auto]; +/** + * Tests a dimensional value against the list of dimension ValueTypes + */ +var findDimensionValueType = function (v) { + return dimensionValueTypes.find(testValueType(v)); +}; + +/** + * A list of all ValueTypes + */ +var valueTypes = tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(dimensionValueTypes), false), [styleValueTypes.color, styleValueTypes.complex], false); +/** + * Tests a value against the list of ValueTypes + */ +var findValueType = function (v) { + return valueTypes.find(testValueType(v)); +}; + +/** + * Set VisualElement's MotionValue, creating a new MotionValue for it if + * it doesn't exist. + */ +function setMotionValue(visualElement, key, value) { + if (visualElement.hasValue(key)) { + visualElement.getValue(key).set(value); + } else { + visualElement.addValue(key, motionValue(value)); + } +} +function setTarget(visualElement, definition) { + var resolved = resolveVariant(visualElement, definition); + var _a = resolved ? visualElement.makeTargetAnimatable(resolved, false) : {}, + _b = _a.transitionEnd, + transitionEnd = _b === void 0 ? {} : _b; + _a.transition; + var target = tslib.__rest(_a, ["transitionEnd", "transition"]); + target = tslib.__assign(tslib.__assign({}, target), transitionEnd); + for (var key in target) { + var value = resolveFinalValueInKeyframes(target[key]); + setMotionValue(visualElement, key, value); + } +} +function setVariants(visualElement, variantLabels) { + var reversedLabels = tslib.__spreadArray([], tslib.__read(variantLabels), false).reverse(); + reversedLabels.forEach(function (key) { + var _a; + var variant = visualElement.getVariant(key); + variant && setTarget(visualElement, variant); + (_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach(function (child) { + setVariants(child, variantLabels); + }); + }); +} +function setValues(visualElement, definition) { + if (Array.isArray(definition)) { + return setVariants(visualElement, definition); + } else if (typeof definition === "string") { + return setVariants(visualElement, [definition]); + } else { + setTarget(visualElement, definition); + } +} +function checkTargetForNewValues(visualElement, target, origin) { + var _a, _b, _c; + var _d; + var newValueKeys = Object.keys(target).filter(function (key) { + return !visualElement.hasValue(key); + }); + var numNewValues = newValueKeys.length; + if (!numNewValues) return; + for (var i = 0; i < numNewValues; i++) { + var key = newValueKeys[i]; + var targetValue = target[key]; + var value = null; + /** + * If the target is a series of keyframes, we can use the first value + * in the array. If this first value is null, we'll still need to read from the DOM. + */ + if (Array.isArray(targetValue)) { + value = targetValue[0]; + } + /** + * If the target isn't keyframes, or the first keyframe was null, we need to + * first check if an origin value was explicitly defined in the transition as "from", + * if not read the value from the DOM. As an absolute fallback, take the defined target value. + */ + if (value === null) { + value = (_b = (_a = origin[key]) !== null && _a !== void 0 ? _a : visualElement.readValue(key)) !== null && _b !== void 0 ? _b : target[key]; + } + /** + * If value is still undefined or null, ignore it. Preferably this would throw, + * but this was causing issues in Framer. + */ + if (value === undefined || value === null) continue; + if (typeof value === "string" && (isNumericalString(value) || isZeroValueString(value))) { + // If this is a number read as a string, ie "0" or "200", convert it to a number + value = parseFloat(value); + } else if (!findValueType(value) && styleValueTypes.complex.test(targetValue)) { + value = getAnimatableNone(key, targetValue); + } + visualElement.addValue(key, motionValue(value)); + (_c = (_d = origin)[key]) !== null && _c !== void 0 ? _c : _d[key] = value; + visualElement.setBaseTarget(key, value); + } +} +function getOriginFromTransition(key, transition) { + if (!transition) return; + var valueTransition = transition[key] || transition["default"] || transition; + return valueTransition.from; +} +function getOrigin(target, transition, visualElement) { + var _a, _b; + var origin = {}; + for (var key in target) { + origin[key] = (_a = getOriginFromTransition(key, transition)) !== null && _a !== void 0 ? _a : (_b = visualElement.getValue(key)) === null || _b === void 0 ? void 0 : _b.get(); + } + return origin; +} +function animateVisualElement(visualElement, definition, options) { + if (options === void 0) { + options = {}; + } + visualElement.notifyAnimationStart(definition); + var animation; + if (Array.isArray(definition)) { + var animations = definition.map(function (variant) { + return animateVariant(visualElement, variant, options); + }); + animation = Promise.all(animations); + } else if (typeof definition === "string") { + animation = animateVariant(visualElement, definition, options); + } else { + var resolvedDefinition = typeof definition === "function" ? resolveVariant(visualElement, definition, options.custom) : definition; + animation = animateTarget(visualElement, resolvedDefinition, options); + } + return animation.then(function () { + return visualElement.notifyAnimationComplete(definition); + }); +} +function animateVariant(visualElement, variant, options) { + var _a; + if (options === void 0) { + options = {}; + } + var resolved = resolveVariant(visualElement, variant, options.custom); + var _b = (resolved || {}).transition, + transition = _b === void 0 ? visualElement.getDefaultTransition() || {} : _b; + if (options.transitionOverride) { + transition = options.transitionOverride; + } + /** + * If we have a variant, create a callback that runs it as an animation. + * Otherwise, we resolve a Promise immediately for a composable no-op. + */ + var getAnimation = resolved ? function () { + return animateTarget(visualElement, resolved, options); + } : function () { + return Promise.resolve(); + }; + /** + * If we have children, create a callback that runs all their animations. + * Otherwise, we resolve a Promise immediately for a composable no-op. + */ + var getChildAnimations = ((_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.size) ? function (forwardDelay) { + if (forwardDelay === void 0) { + forwardDelay = 0; + } + var _a = transition.delayChildren, + delayChildren = _a === void 0 ? 0 : _a, + staggerChildren = transition.staggerChildren, + staggerDirection = transition.staggerDirection; + return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options); + } : function () { + return Promise.resolve(); + }; + /** + * If the transition explicitly defines a "when" option, we need to resolve either + * this animation or all children animations before playing the other. + */ + var when = transition.when; + if (when) { + var _c = tslib.__read(when === "beforeChildren" ? [getAnimation, getChildAnimations] : [getChildAnimations, getAnimation], 2), + first = _c[0], + last = _c[1]; + return first().then(last); + } else { + return Promise.all([getAnimation(), getChildAnimations(options.delay)]); + } +} +/** + * @internal + */ +function animateTarget(visualElement, definition, _a) { + var _b; + var _c = _a === void 0 ? {} : _a, + _d = _c.delay, + delay = _d === void 0 ? 0 : _d, + transitionOverride = _c.transitionOverride, + type = _c.type; + var _e = visualElement.makeTargetAnimatable(definition), + _f = _e.transition, + transition = _f === void 0 ? visualElement.getDefaultTransition() : _f, + transitionEnd = _e.transitionEnd, + target = tslib.__rest(_e, ["transition", "transitionEnd"]); + if (transitionOverride) transition = transitionOverride; + var animations = []; + var animationTypeState = type && ((_b = visualElement.animationState) === null || _b === void 0 ? void 0 : _b.getState()[type]); + for (var key in target) { + var value = visualElement.getValue(key); + var valueTarget = target[key]; + if (!value || valueTarget === undefined || animationTypeState && shouldBlockAnimation(animationTypeState, key)) { + continue; + } + var valueTransition = tslib.__assign({ + delay: delay + }, transition); + /** + * Make animation instant if this is a transform prop and we should reduce motion. + */ + if (visualElement.shouldReduceMotion && isTransformProp(key)) { + valueTransition = tslib.__assign(tslib.__assign({}, valueTransition), { + type: false, + delay: 0 + }); + } + var animation = startAnimation(key, value, valueTarget, valueTransition); + animations.push(animation); + } + return Promise.all(animations).then(function () { + transitionEnd && setTarget(visualElement, transitionEnd); + }); +} +function animateChildren(visualElement, variant, delayChildren, staggerChildren, staggerDirection, options) { + if (delayChildren === void 0) { + delayChildren = 0; + } + if (staggerChildren === void 0) { + staggerChildren = 0; + } + if (staggerDirection === void 0) { + staggerDirection = 1; + } + var animations = []; + var maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren; + var generateStaggerDuration = staggerDirection === 1 ? function (i) { + if (i === void 0) { + i = 0; + } + return i * staggerChildren; + } : function (i) { + if (i === void 0) { + i = 0; + } + return maxStaggerDuration - i * staggerChildren; + }; + Array.from(visualElement.variantChildren).sort(sortByTreeOrder).forEach(function (child, i) { + animations.push(animateVariant(child, variant, tslib.__assign(tslib.__assign({}, options), { + delay: delayChildren + generateStaggerDuration(i) + })).then(function () { + return child.notifyAnimationComplete(variant); + })); + }); + return Promise.all(animations); +} +function stopAnimation(visualElement) { + visualElement.forEachValue(function (value) { + return value.stop(); + }); +} +function sortByTreeOrder(a, b) { + return a.sortNodePosition(b); +} +/** + * Decide whether we should block this animation. Previously, we achieved this + * just by checking whether the key was listed in protectedKeys, but this + * posed problems if an animation was triggered by afterChildren and protectedKeys + * had been set to true in the meantime. + */ +function shouldBlockAnimation(_a, key) { + var protectedKeys = _a.protectedKeys, + needsAnimating = _a.needsAnimating; + var shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true; + needsAnimating[key] = false; + return shouldBlock; +} +var variantPriorityOrder = [exports.AnimationType.Animate, exports.AnimationType.InView, exports.AnimationType.Focus, exports.AnimationType.Hover, exports.AnimationType.Tap, exports.AnimationType.Drag, exports.AnimationType.Exit]; +var reversePriorityOrder = tslib.__spreadArray([], tslib.__read(variantPriorityOrder), false).reverse(); +var numAnimationTypes = variantPriorityOrder.length; +function animateList(visualElement) { + return function (animations) { + return Promise.all(animations.map(function (_a) { + var animation = _a.animation, + options = _a.options; + return animateVisualElement(visualElement, animation, options); + })); + }; +} +function createAnimationState(visualElement) { + var animate = animateList(visualElement); + var state = createState(); + var allAnimatedKeys = {}; + var isInitialRender = true; + /** + * This function will be used to reduce the animation definitions for + * each active animation type into an object of resolved values for it. + */ + var buildResolvedTypeValues = function (acc, definition) { + var resolved = resolveVariant(visualElement, definition); + if (resolved) { + resolved.transition; + var transitionEnd = resolved.transitionEnd, + target = tslib.__rest(resolved, ["transition", "transitionEnd"]); + acc = tslib.__assign(tslib.__assign(tslib.__assign({}, acc), target), transitionEnd); + } + return acc; + }; + function isAnimated(key) { + return allAnimatedKeys[key] !== undefined; + } + /** + * This just allows us to inject mocked animation functions + * @internal + */ + function setAnimateFunction(makeAnimator) { + animate = makeAnimator(visualElement); + } + /** + * When we receive new props, we need to: + * 1. Create a list of protected keys for each type. This is a directory of + * value keys that are currently being "handled" by types of a higher priority + * so that whenever an animation is played of a given type, these values are + * protected from being animated. + * 2. Determine if an animation type needs animating. + * 3. Determine if any values have been removed from a type and figure out + * what to animate those to. + */ + function animateChanges(options, changedActiveType) { + var _a; + var props = visualElement.getProps(); + var context = visualElement.getVariantContext(true) || {}; + /** + * A list of animations that we'll build into as we iterate through the animation + * types. This will get executed at the end of the function. + */ + var animations = []; + /** + * Keep track of which values have been removed. Then, as we hit lower priority + * animation types, we can check if they contain removed values and animate to that. + */ + var removedKeys = new Set(); + /** + * A dictionary of all encountered keys. This is an object to let us build into and + * copy it without iteration. Each time we hit an animation type we set its protected + * keys - the keys its not allowed to animate - to the latest version of this object. + */ + var encounteredKeys = {}; + /** + * If a variant has been removed at a given index, and this component is controlling + * variant animations, we want to ensure lower-priority variants are forced to animate. + */ + var removedVariantIndex = Infinity; + var _loop_1 = function (i) { + var type = reversePriorityOrder[i]; + var typeState = state[type]; + var prop = (_a = props[type]) !== null && _a !== void 0 ? _a : context[type]; + var propIsVariant = isVariantLabel(prop); + /** + * If this type has *just* changed isActive status, set activeDelta + * to that status. Otherwise set to null. + */ + var activeDelta = type === changedActiveType ? typeState.isActive : null; + if (activeDelta === false) removedVariantIndex = i; + /** + * If this prop is an inherited variant, rather than been set directly on the + * component itself, we want to make sure we allow the parent to trigger animations. + * + * TODO: Can probably change this to a !isControllingVariants check + */ + var isInherited = prop === context[type] && prop !== props[type] && propIsVariant; + /** + * + */ + if (isInherited && isInitialRender && visualElement.manuallyAnimateOnMount) { + isInherited = false; + } + /** + * Set all encountered keys so far as the protected keys for this type. This will + * be any key that has been animated or otherwise handled by active, higher-priortiy types. + */ + typeState.protectedKeys = tslib.__assign({}, encounteredKeys); + // Check if we can skip analysing this prop early + if ( + // If it isn't active and hasn't *just* been set as inactive + !typeState.isActive && activeDelta === null || + // If we didn't and don't have any defined prop for this animation type + !prop && !typeState.prevProp || + // Or if the prop doesn't define an animation + isAnimationControls(prop) || typeof prop === "boolean") { + return "continue"; + } + /** + * As we go look through the values defined on this type, if we detect + * a changed value or a value that was removed in a higher priority, we set + * this to true and add this prop to the animation list. + */ + var variantDidChange = checkVariantsDidChange(typeState.prevProp, prop); + var shouldAnimateType = variantDidChange || + // If we're making this variant active, we want to always make it active + type === changedActiveType && typeState.isActive && !isInherited && propIsVariant || + // If we removed a higher-priority variant (i is in reverse order) + i > removedVariantIndex && propIsVariant; + /** + * As animations can be set as variant lists, variants or target objects, we + * coerce everything to an array if it isn't one already + */ + var definitionList = Array.isArray(prop) ? prop : [prop]; + /** + * Build an object of all the resolved values. We'll use this in the subsequent + * animateChanges calls to determine whether a value has changed. + */ + var resolvedValues = definitionList.reduce(buildResolvedTypeValues, {}); + if (activeDelta === false) resolvedValues = {}; + /** + * Now we need to loop through all the keys in the prev prop and this prop, + * and decide: + * 1. If the value has changed, and needs animating + * 2. If it has been removed, and needs adding to the removedKeys set + * 3. If it has been removed in a higher priority type and needs animating + * 4. If it hasn't been removed in a higher priority but hasn't changed, and + * needs adding to the type's protectedKeys list. + */ + var _b = typeState.prevResolvedValues, + prevResolvedValues = _b === void 0 ? {} : _b; + var allKeys = tslib.__assign(tslib.__assign({}, prevResolvedValues), resolvedValues); + var markToAnimate = function (key) { + shouldAnimateType = true; + removedKeys.delete(key); + typeState.needsAnimating[key] = true; + }; + for (var key in allKeys) { + var next = resolvedValues[key]; + var prev = prevResolvedValues[key]; + // If we've already handled this we can just skip ahead + if (encounteredKeys.hasOwnProperty(key)) continue; + /** + * If the value has changed, we probably want to animate it. + */ + if (next !== prev) { + /** + * If both values are keyframes, we need to shallow compare them to + * detect whether any value has changed. If it has, we animate it. + */ + if (isKeyframesTarget(next) && isKeyframesTarget(prev)) { + if (!shallowCompare(next, prev) || variantDidChange) { + markToAnimate(key); + } else { + /** + * If it hasn't changed, we want to ensure it doesn't animate by + * adding it to the list of protected keys. + */ + typeState.protectedKeys[key] = true; + } + } else if (next !== undefined) { + // If next is defined and doesn't equal prev, it needs animating + markToAnimate(key); + } else { + // If it's undefined, it's been removed. + removedKeys.add(key); + } + } else if (next !== undefined && removedKeys.has(key)) { + /** + * If next hasn't changed and it isn't undefined, we want to check if it's + * been removed by a higher priority + */ + markToAnimate(key); + } else { + /** + * If it hasn't changed, we add it to the list of protected values + * to ensure it doesn't get animated. + */ + typeState.protectedKeys[key] = true; + } + } + /** + * Update the typeState so next time animateChanges is called we can compare the + * latest prop and resolvedValues to these. + */ + typeState.prevProp = prop; + typeState.prevResolvedValues = resolvedValues; + /** + * + */ + if (typeState.isActive) { + encounteredKeys = tslib.__assign(tslib.__assign({}, encounteredKeys), resolvedValues); + } + if (isInitialRender && visualElement.blockInitialAnimation) { + shouldAnimateType = false; + } + /** + * If this is an inherited prop we want to hard-block animations + * TODO: Test as this should probably still handle animations triggered + * by removed values? + */ + if (shouldAnimateType && !isInherited) { + animations.push.apply(animations, tslib.__spreadArray([], tslib.__read(definitionList.map(function (animation) { + return { + animation: animation, + options: tslib.__assign({ + type: type + }, options) + }; + })), false)); + } + }; + /** + * Iterate through all animation types in reverse priority order. For each, we want to + * detect which values it's handling and whether or not they've changed (and therefore + * need to be animated). If any values have been removed, we want to detect those in + * lower priority props and flag for animation. + */ + for (var i = 0; i < numAnimationTypes; i++) { + _loop_1(i); + } + allAnimatedKeys = tslib.__assign({}, encounteredKeys); + /** + * If there are some removed value that haven't been dealt with, + * we need to create a new animation that falls back either to the value + * defined in the style prop, or the last read value. + */ + if (removedKeys.size) { + var fallbackAnimation_1 = {}; + removedKeys.forEach(function (key) { + var fallbackTarget = visualElement.getBaseTarget(key); + if (fallbackTarget !== undefined) { + fallbackAnimation_1[key] = fallbackTarget; + } + }); + animations.push({ + animation: fallbackAnimation_1 + }); + } + var shouldAnimate = Boolean(animations.length); + if (isInitialRender && props.initial === false && !visualElement.manuallyAnimateOnMount) { + shouldAnimate = false; + } + isInitialRender = false; + return shouldAnimate ? animate(animations) : Promise.resolve(); + } + /** + * Change whether a certain animation type is active. + */ + function setActive(type, isActive, options) { + var _a; + // If the active state hasn't changed, we can safely do nothing here + if (state[type].isActive === isActive) return Promise.resolve(); + // Propagate active change to children + (_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach(function (child) { + var _a; + return (_a = child.animationState) === null || _a === void 0 ? void 0 : _a.setActive(type, isActive); + }); + state[type].isActive = isActive; + var animations = animateChanges(options, type); + for (var key in state) { + state[key].protectedKeys = {}; + } + return animations; + } + return { + isAnimated: isAnimated, + animateChanges: animateChanges, + setActive: setActive, + setAnimateFunction: setAnimateFunction, + getState: function () { + return state; + } + }; +} +function checkVariantsDidChange(prev, next) { + if (typeof next === "string") { + return next !== prev; + } else if (isVariantLabels(next)) { + return !shallowCompare(next, prev); + } + return false; +} +function createTypeState(isActive) { + if (isActive === void 0) { + isActive = false; + } + return { + isActive: isActive, + protectedKeys: {}, + needsAnimating: {}, + prevResolvedValues: {} + }; +} +function createState() { + var _a; + return _a = {}, _a[exports.AnimationType.Animate] = createTypeState(true), _a[exports.AnimationType.InView] = createTypeState(), _a[exports.AnimationType.Hover] = createTypeState(), _a[exports.AnimationType.Tap] = createTypeState(), _a[exports.AnimationType.Drag] = createTypeState(), _a[exports.AnimationType.Focus] = createTypeState(), _a[exports.AnimationType.Exit] = createTypeState(), _a; +} +var animations = { + animation: makeRenderlessComponent(function (_a) { + var visualElement = _a.visualElement, + animate = _a.animate; + /** + * We dynamically generate the AnimationState manager as it contains a reference + * to the underlying animation library. We only want to load that if we load this, + * so people can optionally code split it out using the `m` component. + */ + visualElement.animationState || (visualElement.animationState = createAnimationState(visualElement)); + /** + * Subscribe any provided AnimationControls to the component's VisualElement + */ + if (isAnimationControls(animate)) { + React.useEffect(function () { + return animate.subscribe(visualElement); + }, [animate]); + } + }), + exit: makeRenderlessComponent(function (props) { + var custom = props.custom, + visualElement = props.visualElement; + var _a = tslib.__read(usePresence(), 2), + isPresent = _a[0], + safeToRemove = _a[1]; + var presenceContext = React.useContext(PresenceContext); + React.useEffect(function () { + var _a, _b; + visualElement.isPresent = isPresent; + var animation = (_a = visualElement.animationState) === null || _a === void 0 ? void 0 : _a.setActive(exports.AnimationType.Exit, !isPresent, { + custom: (_b = presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.custom) !== null && _b !== void 0 ? _b : custom + }); + !isPresent && (animation === null || animation === void 0 ? void 0 : animation.then(safeToRemove)); + }, [isPresent]); + }) +}; + +/** + * @internal + */ +var PanSession = /** @class */function () { + function PanSession(event, handlers, _a) { + var _this = this; + var _b = _a === void 0 ? {} : _a, + transformPagePoint = _b.transformPagePoint; + /** + * @internal + */ + this.startEvent = null; + /** + * @internal + */ + this.lastMoveEvent = null; + /** + * @internal + */ + this.lastMoveEventInfo = null; + /** + * @internal + */ + this.handlers = {}; + this.updatePoint = function () { + if (!(_this.lastMoveEvent && _this.lastMoveEventInfo)) return; + var info = getPanInfo(_this.lastMoveEventInfo, _this.history); + var isPanStarted = _this.startEvent !== null; + // Only start panning if the offset is larger than 3 pixels. If we make it + // any larger than this we'll want to reset the pointer history + // on the first update to avoid visual snapping to the cursoe. + var isDistancePastThreshold = popmotion.distance(info.offset, { + x: 0, + y: 0 + }) >= 3; + if (!isPanStarted && !isDistancePastThreshold) return; + var point = info.point; + var timestamp = sync.getFrameData().timestamp; + _this.history.push(tslib.__assign(tslib.__assign({}, point), { + timestamp: timestamp + })); + var _a = _this.handlers, + onStart = _a.onStart, + onMove = _a.onMove; + if (!isPanStarted) { + onStart && onStart(_this.lastMoveEvent, info); + _this.startEvent = _this.lastMoveEvent; + } + onMove && onMove(_this.lastMoveEvent, info); + }; + this.handlePointerMove = function (event, info) { + _this.lastMoveEvent = event; + _this.lastMoveEventInfo = transformPoint(info, _this.transformPagePoint); + // Because Safari doesn't trigger mouseup events when it's above a ` ' + e.phrase("(Use line:column or scroll% syntax)") + ""; + } + c(i, "getJumpDialog"); + function a(e, t) { + var n = Number(t); + return /^[-+]/.test(t) ? e.getCursor().line + n : n - 1; + } + c(a, "interpretLine"), o.commands.jumpToLine = function (e) { + var t = e.getCursor(); + s(e, i(e), e.phrase("Jump to line:"), t.line + 1 + ":" + t.ch, function (n) { + if (n) { + var r; + if (r = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(n)) e.setCursor(a(e, r[1]), Number(r[2]));else if (r = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(n)) { + var l = Math.round(e.lineCount() * Number(r[1]) / 100); + /^[-+]/.test(r[1]) && (l = t.line + l + 1), e.setCursor(l - 1, t.ch); + } else (r = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(n)) && e.setCursor(a(e, r[1]), t.ch); + } + }); + }, o.keyMap.default["Alt-G"] = "jumpToLine"; + }); +})(); +var d = b.exports; +const j = f.getDefaultExportFromCjs(d), + y = h({ + __proto__: null, + default: j + }, [d]); +exports.jumpToLine = y; + +/***/ }), + +/***/ "../../graphiql-react/dist/jump.cjs.js": +/*!*********************************************!*\ + !*** ../../graphiql-react/dist/jump.cjs.js ***! + \*********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var c = Object.defineProperty; +var s = (e, r) => c(e, "name", { + value: r, + configurable: !0 +}); +const u = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + d = __webpack_require__(/*! ./SchemaReference.cjs.js */ "../../graphiql-react/dist/SchemaReference.cjs.js"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +__webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +__webpack_require__(/*! ./forEachState.cjs.js */ "../../graphiql-react/dist/forEachState.cjs.js"); +u.CodeMirror.defineOption("jump", !1, (e, r, n) => { + if (n && n !== u.CodeMirror.Init) { + const t = e.state.jump.onMouseOver; + u.CodeMirror.off(e.getWrapperElement(), "mouseover", t); + const i = e.state.jump.onMouseOut; + u.CodeMirror.off(e.getWrapperElement(), "mouseout", i), u.CodeMirror.off(document, "keydown", e.state.jump.onKeyDown), delete e.state.jump; + } + if (r) { + const t = e.state.jump = { + options: r, + onMouseOver: M.bind(null, e), + onMouseOut: m.bind(null, e), + onKeyDown: g.bind(null, e) + }; + u.CodeMirror.on(e.getWrapperElement(), "mouseover", t.onMouseOver), u.CodeMirror.on(e.getWrapperElement(), "mouseout", t.onMouseOut), u.CodeMirror.on(document, "keydown", t.onKeyDown); + } +}); +function M(e, r) { + const n = r.target || r.srcElement; + if (!(n instanceof HTMLElement) || (n == null ? void 0 : n.nodeName) !== "SPAN") return; + const t = n.getBoundingClientRect(), + i = { + left: (t.left + t.right) / 2, + top: (t.top + t.bottom) / 2 + }; + e.state.jump.cursor = i, e.state.jump.isHoldingModifier && l(e); +} +s(M, "onMouseOver"); +function m(e) { + if (!e.state.jump.isHoldingModifier && e.state.jump.cursor) { + e.state.jump.cursor = null; + return; + } + e.state.jump.isHoldingModifier && e.state.jump.marker && p(e); +} +s(m, "onMouseOut"); +function g(e, r) { + if (e.state.jump.isHoldingModifier || !k(r.key)) return; + e.state.jump.isHoldingModifier = !0, e.state.jump.cursor && l(e); + const n = s(o => { + o.code === r.code && (e.state.jump.isHoldingModifier = !1, e.state.jump.marker && p(e), u.CodeMirror.off(document, "keyup", n), u.CodeMirror.off(document, "click", t), e.off("mousedown", i)); + }, "onKeyUp"), + t = s(o => { + const { + destination: a, + options: f + } = e.state.jump; + a && f.onClick(a, o); + }, "onClick"), + i = s((o, a) => { + e.state.jump.destination && (a.codemirrorIgnore = !0); + }, "onMouseDown"); + u.CodeMirror.on(document, "keyup", n), u.CodeMirror.on(document, "click", t), e.on("mousedown", i); +} +s(g, "onKeyDown"); +const j = typeof navigator < "u" && navigator && navigator.appVersion.includes("Mac"); +function k(e) { + return e === (j ? "Meta" : "Control"); +} +s(k, "isJumpModifier"); +function l(e) { + if (e.state.jump.marker) return; + const { + cursor: r, + options: n + } = e.state.jump, + t = e.coordsChar(r), + i = e.getTokenAt(t, !0), + o = n.getDestination || e.getHelper(t, "jump"); + if (o) { + const a = o(i, n, e); + if (a) { + const f = e.markText({ + line: t.line, + ch: i.start + }, { + line: t.line, + ch: i.end + }, { + className: "CodeMirror-jump-token" + }); + e.state.jump.marker = f, e.state.jump.destination = a; + } + } +} +s(l, "enableJumpMode"); +function p(e) { + const { + marker: r + } = e.state.jump; + e.state.jump.marker = null, e.state.jump.destination = null, r.clear(); +} +s(p, "disableJumpMode"); +u.CodeMirror.registerHelper("jump", "graphql", (e, r) => { + if (!r.schema || !r.onClick || !e.state) return; + const { + state: n + } = e, + { + kind: t, + step: i + } = n, + o = d.getTypeInfo(r.schema, n); + if (t === "Field" && i === 0 && o.fieldDef || t === "AliasedField" && i === 2 && o.fieldDef) return d.getFieldReference(o); + if (t === "Directive" && i === 1 && o.directiveDef) return d.getDirectiveReference(o); + if (t === "Argument" && i === 0 && o.argDef) return d.getArgumentReference(o); + if (t === "EnumValue" && o.enumValue) return d.getEnumValueReference(o); + if (t === "NamedType" && o.type) return d.getTypeReference(o); +}); + +/***/ }), + +/***/ "../../graphiql-react/dist/lint.cjs.js": +/*!*********************************************!*\ + !*** ../../graphiql-react/dist/lint.cjs.js ***! + \*********************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var W = Object.defineProperty; +var s = (h, v) => W(h, "name", { + value: v, + configurable: !0 +}); +const x = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +function q(h, v) { + for (var l = 0; l < v.length; l++) { + const u = v[l]; + if (typeof u != "string" && !Array.isArray(u)) { + for (const g in u) if (g !== "default" && !(g in h)) { + const c = Object.getOwnPropertyDescriptor(u, g); + c && Object.defineProperty(h, g, c.get ? c : { + enumerable: !0, + get: () => u[g] + }); + } + } + } + return Object.freeze(Object.defineProperty(h, Symbol.toStringTag, { + value: "Module" + })); +} +s(q, "_mergeNamespaces"); +var B = { + exports: {} +}; +(function (h, v) { + (function (l) { + l(x.requireCodemirror()); + })(function (l) { + var u = "CodeMirror-lint-markers", + g = "CodeMirror-lint-line-"; + function c(t, e, r) { + var n = document.createElement("div"); + n.className = "CodeMirror-lint-tooltip cm-s-" + t.options.theme, n.appendChild(r.cloneNode(!0)), t.state.lint.options.selfContain ? t.getWrapperElement().appendChild(n) : document.body.appendChild(n); + function i(o) { + if (!n.parentNode) return l.off(document, "mousemove", i); + n.style.top = Math.max(0, o.clientY - n.offsetHeight - 5) + "px", n.style.left = o.clientX + 5 + "px"; + } + return s(i, "position"), l.on(document, "mousemove", i), i(e), n.style.opacity != null && (n.style.opacity = 1), n; + } + s(c, "showTooltip"); + function L(t) { + t.parentNode && t.parentNode.removeChild(t); + } + s(L, "rm"); + function A(t) { + t.parentNode && (t.style.opacity == null && L(t), t.style.opacity = 0, setTimeout(function () { + L(t); + }, 600)); + } + s(A, "hideTooltip"); + function M(t, e, r, n) { + var i = c(t, e, r); + function o() { + l.off(n, "mouseout", o), i && (A(i), i = null); + } + s(o, "hide"); + var a = setInterval(function () { + if (i) for (var f = n;; f = f.parentNode) { + if (f && f.nodeType == 11 && (f = f.host), f == document.body) return; + if (!f) { + o(); + break; + } + } + if (!i) return clearInterval(a); + }, 400); + l.on(n, "mouseout", o); + } + s(M, "showTooltipFor"); + function F(t, e, r) { + this.marked = [], e instanceof Function && (e = { + getAnnotations: e + }), (!e || e === !0) && (e = {}), this.options = {}, this.linterOptions = e.options || {}; + for (var n in C) this.options[n] = C[n]; + for (var n in e) C.hasOwnProperty(n) ? e[n] != null && (this.options[n] = e[n]) : e.options || (this.linterOptions[n] = e[n]); + this.timeout = null, this.hasGutter = r, this.onMouseOver = function (i) { + U(t, i); + }, this.waitingFor = 0; + } + s(F, "LintState"); + var C = { + highlightLines: !1, + tooltips: !0, + delay: 500, + lintOnChange: !0, + getAnnotations: null, + async: !1, + selfContain: null, + formatAnnotation: null, + onUpdateLinting: null + }; + function E(t) { + var e = t.state.lint; + e.hasGutter && t.clearGutter(u), e.options.highlightLines && G(t); + for (var r = 0; r < e.marked.length; ++r) e.marked[r].clear(); + e.marked.length = 0; + } + s(E, "clearMarks"); + function G(t) { + t.eachLine(function (e) { + var r = e.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(e.wrapClass); + r && t.removeLineClass(e, "wrap", r[0]); + }); + } + s(G, "clearErrorLines"); + function I(t, e, r, n, i) { + var o = document.createElement("div"), + a = o; + return o.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + r, n && (a = o.appendChild(document.createElement("div")), a.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple"), i != !1 && l.on(a, "mouseover", function (f) { + M(t, f, e, a); + }), o; + } + s(I, "makeMarker"); + function D(t, e) { + return t == "error" ? t : e; + } + s(D, "getMaxSeverity"); + function j(t) { + for (var e = [], r = 0; r < t.length; ++r) { + var n = t[r], + i = n.from.line; + (e[i] || (e[i] = [])).push(n); + } + return e; + } + s(j, "groupByLine"); + function N(t) { + var e = t.severity; + e || (e = "error"); + var r = document.createElement("div"); + return r.className = "CodeMirror-lint-message CodeMirror-lint-message-" + e, typeof t.messageHTML < "u" ? r.innerHTML = t.messageHTML : r.appendChild(document.createTextNode(t.message)), r; + } + s(N, "annotationTooltip"); + function H(t, e) { + var r = t.state.lint, + n = ++r.waitingFor; + function i() { + n = -1, t.off("change", i); + } + s(i, "abort"), t.on("change", i), e(t.getValue(), function (o, a) { + t.off("change", i), r.waitingFor == n && (a && o instanceof l && (o = a), t.operation(function () { + O(t, o); + })); + }, r.linterOptions, t); + } + s(H, "lintAsync"); + function k(t) { + var e = t.state.lint; + if (e) { + var r = e.options, + n = r.getAnnotations || t.getHelper(l.Pos(0, 0), "lint"); + if (n) if (r.async || n.async) H(t, n);else { + var i = n(t.getValue(), e.linterOptions, t); + if (!i) return; + i.then ? i.then(function (o) { + t.operation(function () { + O(t, o); + }); + }) : t.operation(function () { + O(t, i); + }); + } + } + } + s(k, "startLinting"); + function O(t, e) { + var r = t.state.lint; + if (r) { + var n = r.options; + E(t); + for (var i = j(e), o = 0; o < i.length; ++o) { + var a = i[o]; + if (a) { + var f = []; + a = a.filter(function (w) { + return f.indexOf(w.message) > -1 ? !1 : f.push(w.message); + }); + for (var p = null, m = r.hasGutter && document.createDocumentFragment(), T = 0; T < a.length; ++T) { + var d = a[T], + y = d.severity; + y || (y = "error"), p = D(p, y), n.formatAnnotation && (d = n.formatAnnotation(d)), r.hasGutter && m.appendChild(N(d)), d.to && r.marked.push(t.markText(d.from, d.to, { + className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + y, + __annotation: d + })); + } + r.hasGutter && t.setGutterMarker(o, u, I(t, m, p, i[o].length > 1, n.tooltips)), n.highlightLines && t.addLineClass(o, "wrap", g + p); + } + } + n.onUpdateLinting && n.onUpdateLinting(e, i, t); + } + } + s(O, "updateLinting"); + function b(t) { + var e = t.state.lint; + e && (clearTimeout(e.timeout), e.timeout = setTimeout(function () { + k(t); + }, e.options.delay)); + } + s(b, "onChange"); + function P(t, e, r) { + for (var n = r.target || r.srcElement, i = document.createDocumentFragment(), o = 0; o < e.length; o++) { + var a = e[o]; + i.appendChild(N(a)); + } + M(t, r, i, n); + } + s(P, "popupTooltips"); + function U(t, e) { + var r = e.target || e.srcElement; + if (/\bCodeMirror-lint-mark-/.test(r.className)) { + for (var n = r.getBoundingClientRect(), i = (n.left + n.right) / 2, o = (n.top + n.bottom) / 2, a = t.findMarksAt(t.coordsChar({ + left: i, + top: o + }, "client")), f = [], p = 0; p < a.length; ++p) { + var m = a[p].__annotation; + m && f.push(m); + } + f.length && P(t, f, e); + } + } + s(U, "onMouseOver"), l.defineOption("lint", !1, function (t, e, r) { + if (r && r != l.Init && (E(t), t.state.lint.options.lintOnChange !== !1 && t.off("change", b), l.off(t.getWrapperElement(), "mouseover", t.state.lint.onMouseOver), clearTimeout(t.state.lint.timeout), delete t.state.lint), e) { + for (var n = t.getOption("gutters"), i = !1, o = 0; o < n.length; ++o) n[o] == u && (i = !0); + var a = t.state.lint = new F(t, e, i); + a.options.lintOnChange && t.on("change", b), a.options.tooltips != !1 && a.options.tooltips != "gutter" && l.on(t.getWrapperElement(), "mouseover", a.onMouseOver), k(t); + } + }), l.defineExtension("performLint", function () { + k(this); + }); + }); +})(); +var _ = B.exports; +const R = x.getDefaultExportFromCjs(_), + V = q({ + __proto__: null, + default: R + }, [_]); +exports.lint = V; + +/***/ }), + +/***/ "../../graphiql-react/dist/lint.cjs2.js": +/*!**********************************************!*\ + !*** ../../graphiql-react/dist/lint.cjs2.js ***! + \**********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +const t = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + c = __webpack_require__(/*! graphql-language-service */ "../../graphql-language-service/esm/index.js"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +const a = ["error", "warning", "information", "hint"], + g = { + "GraphQL: Validation": "validation", + "GraphQL: Deprecation": "deprecation", + "GraphQL: Syntax": "syntax" + }; +t.CodeMirror.registerHelper("lint", "graphql", (n, s) => { + const { + schema: r, + validationRules: i, + externalFragments: o + } = s; + return c.getDiagnostics(n, r, i, void 0, o).map(e => ({ + message: e.message, + severity: e.severity ? a[e.severity - 1] : a[0], + type: e.source ? g[e.source] : void 0, + from: t.CodeMirror.Pos(e.range.start.line, e.range.start.character), + to: t.CodeMirror.Pos(e.range.end.line, e.range.end.character) + })); +}); + +/***/ }), + +/***/ "../../graphiql-react/dist/lint.cjs3.js": +/*!**********************************************!*\ + !*** ../../graphiql-react/dist/lint.cjs3.js ***! + \**********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var V = Object.defineProperty; +var t = (e, n) => V(e, "name", { + value: n, + configurable: !0 +}); +const I = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + b = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +function C(e) { + d = e, E = e.length, s = u = N = -1, o(), y(); + const n = q(); + return p("EOF"), n; +} +t(C, "jsonParse"); +let d, E, s, u, N, r, l; +function q() { + const e = s, + n = []; + if (p("{"), !x("}")) { + do n.push(M()); while (x(",")); + p("}"); + } + return { + kind: "Object", + start: e, + end: N, + members: n + }; +} +t(q, "parseObj"); +function M() { + const e = s, + n = l === "String" ? G() : null; + p("String"), p(":"); + const i = B(); + return { + kind: "Member", + start: e, + end: N, + key: n, + value: i + }; +} +t(M, "parseMember"); +function v() { + const e = s, + n = []; + if (p("["), !x("]")) { + do n.push(B()); while (x(",")); + p("]"); + } + return { + kind: "Array", + start: e, + end: N, + values: n + }; +} +t(v, "parseArr"); +function B() { + switch (l) { + case "[": + return v(); + case "{": + return q(); + case "String": + case "Number": + case "Boolean": + case "Null": + const e = G(); + return y(), e; + } + p("Value"); +} +t(B, "parseVal"); +function G() { + return { + kind: l, + start: s, + end: u, + value: JSON.parse(d.slice(s, u)) + }; +} +t(G, "curToken"); +function p(e) { + if (l === e) { + y(); + return; + } + let n; + if (l === "EOF") n = "[end of file]";else if (u - s > 1) n = "`" + d.slice(s, u) + "`";else { + const i = d.slice(s).match(/^.+?\b/); + n = "`" + (i ? i[0] : d[s]) + "`"; + } + throw k(`Expected ${e} but found ${n}.`); +} +t(p, "expect"); +class j extends Error { + constructor(n, i) { + super(n), this.position = i; + } +} +t(j, "JSONSyntaxError"); +function k(e) { + return new j(e, { + start: s, + end: u + }); +} +t(k, "syntaxError"); +function x(e) { + if (l === e) return y(), !0; +} +t(x, "skip"); +function o() { + return u < E && (u++, r = u === E ? 0 : d.charCodeAt(u)), r; +} +t(o, "ch"); +function y() { + for (N = u; r === 9 || r === 10 || r === 13 || r === 32;) o(); + if (r === 0) { + l = "EOF"; + return; + } + switch (s = u, r) { + case 34: + return l = "String", D(); + case 45: + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + return l = "Number", H(); + case 102: + if (d.slice(s, s + 5) !== "false") break; + u += 4, o(), l = "Boolean"; + return; + case 110: + if (d.slice(s, s + 4) !== "null") break; + u += 3, o(), l = "Null"; + return; + case 116: + if (d.slice(s, s + 4) !== "true") break; + u += 3, o(), l = "Boolean"; + return; + } + l = d[s], o(); +} +t(y, "lex"); +function D() { + for (o(); r !== 34 && r > 31;) if (r === 92) switch (r = o(), r) { + case 34: + case 47: + case 92: + case 98: + case 102: + case 110: + case 114: + case 116: + o(); + break; + case 117: + o(), w(), w(), w(), w(); + break; + default: + throw k("Bad character escape sequence."); + } else { + if (u === E) throw k("Unterminated string."); + o(); + } + if (r === 34) { + o(); + return; + } + throw k("Unterminated string."); +} +t(D, "readString"); +function w() { + if (r >= 48 && r <= 57 || r >= 65 && r <= 70 || r >= 97 && r <= 102) return o(); + throw k("Expected hexadecimal digit."); +} +t(w, "readHex"); +function H() { + r === 45 && o(), r === 48 ? o() : $(), r === 46 && (o(), $()), (r === 69 || r === 101) && (r = o(), (r === 43 || r === 45) && o(), $()); +} +t(H, "readNumber"); +function $() { + if (r < 48 || r > 57) throw k("Expected decimal digit."); + do o(); while (r >= 48 && r <= 57); +} +t($, "readDigits"); +I.CodeMirror.registerHelper("lint", "graphql-variables", (e, n, i) => { + if (!e) return []; + let f; + try { + f = C(e); + } catch (c) { + if (c instanceof j) return [F(i, c.position, c.message)]; + throw c; + } + const { + variableToType: a + } = n; + return a ? U(i, a, f) : []; +}); +function U(e, n, i) { + var f; + const a = []; + for (const c of i.members) if (c) { + const h = (f = c.key) === null || f === void 0 ? void 0 : f.value, + m = n[h]; + if (m) for (const [O, Q] of g(m, c.value)) a.push(F(e, O, Q));else a.push(F(e, c.key, `Variable "$${h}" does not appear in any GraphQL query.`)); + } + return a; +} +t(U, "validateVariables"); +function g(e, n) { + if (!e || !n) return []; + if (e instanceof b.GraphQLNonNull) return n.kind === "Null" ? [[n, `Type "${e}" is non-nullable and cannot be null.`]] : g(e.ofType, n); + if (n.kind === "Null") return []; + if (e instanceof b.GraphQLList) { + const i = e.ofType; + if (n.kind === "Array") { + const f = n.values || []; + return L(f, a => g(i, a)); + } + return g(i, n); + } + if (e instanceof b.GraphQLInputObjectType) { + if (n.kind !== "Object") return [[n, `Type "${e}" must be an Object.`]]; + const i = Object.create(null), + f = L(n.members, a => { + var c; + const h = (c = a == null ? void 0 : a.key) === null || c === void 0 ? void 0 : c.value; + i[h] = !0; + const m = e.getFields()[h]; + if (!m) return [[a.key, `Type "${e}" does not have a field "${h}".`]]; + const O = m ? m.type : void 0; + return g(O, a.value); + }); + for (const a of Object.keys(e.getFields())) { + const c = e.getFields()[a]; + !i[a] && c.type instanceof b.GraphQLNonNull && !c.defaultValue && f.push([n, `Object of type "${e}" is missing required field "${a}".`]); + } + return f; + } + return e.name === "Boolean" && n.kind !== "Boolean" || e.name === "String" && n.kind !== "String" || e.name === "ID" && n.kind !== "Number" && n.kind !== "String" || e.name === "Float" && n.kind !== "Number" || e.name === "Int" && (n.kind !== "Number" || (n.value | 0) !== n.value) ? [[n, `Expected value of type "${e}".`]] : (e instanceof b.GraphQLEnumType || e instanceof b.GraphQLScalarType) && (n.kind !== "String" && n.kind !== "Number" && n.kind !== "Boolean" && n.kind !== "Null" || _(e.parseValue(n.value))) ? [[n, `Expected value of type "${e}".`]] : []; +} +t(g, "validateValue"); +function F(e, n, i) { + return { + message: i, + severity: "error", + type: "validation", + from: e.posFromIndex(n.start), + to: e.posFromIndex(n.end) + }; +} +t(F, "lintError"); +function _(e) { + return e == null || e !== e; +} +t(_, "isNullish"); +function L(e, n) { + return Array.prototype.concat.apply([], e.map(n)); +} +t(L, "mapCat"); + +/***/ }), + +/***/ "../../graphiql-react/dist/matchbrackets.cjs.js": +/*!******************************************************!*\ + !*** ../../graphiql-react/dist/matchbrackets.cjs.js ***! + \******************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var i = Object.defineProperty; +var s = (e, c) => i(e, "name", { + value: c, + configurable: !0 +}); +const u = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"), + f = __webpack_require__(/*! ./matchbrackets.cjs2.js */ "../../graphiql-react/dist/matchbrackets.cjs2.js"); +function b(e, c) { + for (var o = 0; o < c.length; o++) { + const t = c[o]; + if (typeof t != "string" && !Array.isArray(t)) { + for (const r in t) if (r !== "default" && !(r in e)) { + const a = Object.getOwnPropertyDescriptor(t, r); + a && Object.defineProperty(e, r, a.get ? a : { + enumerable: !0, + get: () => t[r] + }); + } + } + } + return Object.freeze(Object.defineProperty(e, Symbol.toStringTag, { + value: "Module" + })); +} +s(b, "_mergeNamespaces"); +var n = f.requireMatchbrackets(); +const l = u.getDefaultExportFromCjs(n), + m = b({ + __proto__: null, + default: l + }, [n]); +exports.matchbrackets = m; + +/***/ }), + +/***/ "../../graphiql-react/dist/matchbrackets.cjs2.js": +/*!*******************************************************!*\ + !*** ../../graphiql-react/dist/matchbrackets.cjs2.js ***! + \*******************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var R = Object.defineProperty; +var f = (L, y) => R(L, "name", { + value: y, + configurable: !0 +}); +const F = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +var T = { + exports: {} + }, + E; +function I() { + return E || (E = 1, function (L, y) { + (function (o) { + o(F.requireCodemirror()); + })(function (o) { + var S = /MSIE \d/.test(navigator.userAgent) && (document.documentMode == null || document.documentMode < 8), + g = o.Pos, + B = { + "(": ")>", + ")": "(<", + "[": "]>", + "]": "[<", + "{": "}>", + "}": "{<", + "<": ">>", + ">": "<<" + }; + function A(t) { + return t && t.bracketRegex || /[(){}[\]]/; + } + f(A, "bracketRegex"); + function b(t, r, e) { + var s = t.getLineHandle(r.line), + n = r.ch - 1, + h = e && e.afterCursor; + h == null && (h = /(^| )cm-fat-cursor($| )/.test(t.getWrapperElement().className)); + var l = A(e), + u = !h && n >= 0 && l.test(s.text.charAt(n)) && B[s.text.charAt(n)] || l.test(s.text.charAt(n + 1)) && B[s.text.charAt(++n)]; + if (!u) return null; + var a = u.charAt(1) == ">" ? 1 : -1; + if (e && e.strict && a > 0 != (n == r.ch)) return null; + var k = t.getTokenTypeAt(g(r.line, n + 1)), + i = H(t, g(r.line, n + (a > 0 ? 1 : 0)), a, k, e); + return i == null ? null : { + from: g(r.line, n), + to: i && i.pos, + match: i && i.ch == u.charAt(0), + forward: a > 0 + }; + } + f(b, "findMatchingBracket"); + function H(t, r, e, s, n) { + for (var h = n && n.maxScanLineLength || 1e4, l = n && n.maxScanLines || 1e3, u = [], a = A(n), k = e > 0 ? Math.min(r.line + l, t.lastLine() + 1) : Math.max(t.firstLine() - 1, r.line - l), i = r.line; i != k; i += e) { + var c = t.getLine(i); + if (c) { + var v = e > 0 ? 0 : c.length - 1, + q = e > 0 ? c.length : -1; + if (!(c.length > h)) for (i == r.line && (v = r.ch - (e < 0 ? 1 : 0)); v != q; v += e) { + var d = c.charAt(v); + if (a.test(d) && (s === void 0 || (t.getTokenTypeAt(g(i, v + 1)) || "") == (s || ""))) { + var m = B[d]; + if (m && m.charAt(1) == ">" == e > 0) u.push(d);else if (u.length) u.pop();else return { + pos: g(i, v), + ch: d + }; + } + } + } + } + return i - e == (e > 0 ? t.lastLine() : t.firstLine()) ? !1 : null; + } + f(H, "scanForBracket"); + function M(t, r, e) { + for (var s = t.state.matchBrackets.maxHighlightLineLength || 1e3, n = e && e.highlightNonMatching, h = [], l = t.listSelections(), u = 0; u < l.length; u++) { + var a = l[u].empty() && b(t, l[u].head, e); + if (a && (a.match || n !== !1) && t.getLine(a.from.line).length <= s) { + var k = a.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + h.push(t.markText(a.from, g(a.from.line, a.from.ch + 1), { + className: k + })), a.to && t.getLine(a.to.line).length <= s && h.push(t.markText(a.to, g(a.to.line, a.to.ch + 1), { + className: k + })); + } + } + if (h.length) { + S && t.state.focused && t.focus(); + var i = f(function () { + t.operation(function () { + for (var c = 0; c < h.length; c++) h[c].clear(); + }); + }, "clear"); + if (r) setTimeout(i, 800);else return i; + } + } + f(M, "matchBrackets"); + function x(t) { + t.operation(function () { + t.state.matchBrackets.currentlyHighlighted && (t.state.matchBrackets.currentlyHighlighted(), t.state.matchBrackets.currentlyHighlighted = null), t.state.matchBrackets.currentlyHighlighted = M(t, !1, t.state.matchBrackets); + }); + } + f(x, "doMatchBrackets"); + function p(t) { + t.state.matchBrackets && t.state.matchBrackets.currentlyHighlighted && (t.state.matchBrackets.currentlyHighlighted(), t.state.matchBrackets.currentlyHighlighted = null); + } + f(p, "clearHighlighted"), o.defineOption("matchBrackets", !1, function (t, r, e) { + e && e != o.Init && (t.off("cursorActivity", x), t.off("focus", x), t.off("blur", p), p(t)), r && (t.state.matchBrackets = typeof r == "object" ? r : {}, t.on("cursorActivity", x), t.on("focus", x), t.on("blur", p)); + }), o.defineExtension("matchBrackets", function () { + M(this, !0); + }), o.defineExtension("findMatchingBracket", function (t, r, e) { + return (e || typeof r == "boolean") && (e ? (e.strict = r, r = e) : r = r ? { + strict: !0 + } : null), b(this, t, r); + }), o.defineExtension("scanForBracket", function (t, r, e, s) { + return H(this, t, r, e, s); + }); + }); + }()), T.exports; +} +f(I, "requireMatchbrackets"); +exports.requireMatchbrackets = I; + +/***/ }), + +/***/ "../../graphiql-react/dist/mode-indent.cjs.js": +/*!****************************************************!*\ + !*** ../../graphiql-react/dist/mode-indent.cjs.js ***! + \****************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +var o = Object.defineProperty; +var v = (n, t) => o(n, "name", { + value: t, + configurable: !0 +}); +function s(n, t) { + var e, i; + const { + levels: l, + indentLevel: d + } = n; + return ((!l || l.length === 0 ? d : l.at(-1) - (!((e = this.electricInput) === null || e === void 0) && e.test(t) ? 1 : 0)) || 0) * (((i = this.config) === null || i === void 0 ? void 0 : i.indentUnit) || 0); +} +v(s, "indent"); +exports.indent = s; + +/***/ }), + +/***/ "../../graphiql-react/dist/mode.cjs.js": +/*!*********************************************!*\ + !*** ../../graphiql-react/dist/mode.cjs.js ***! + \*********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var n = Object.defineProperty; +var s = (e, r) => n(e, "name", { + value: r, + configurable: !0 +}); +const o = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + t = __webpack_require__(/*! graphql-language-service */ "../../graphql-language-service/esm/index.js"), + i = __webpack_require__(/*! ./mode-indent.cjs.js */ "../../graphiql-react/dist/mode-indent.cjs.js"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +const l = s(e => { + const r = t.onlineParser({ + eatWhitespace: a => a.eatWhile(t.isIgnored), + lexRules: t.LexRules, + parseRules: t.ParseRules, + editorConfig: { + tabSize: e.tabSize + } + }); + return { + config: e, + startState: r.startState, + token: r.token, + indent: i.indent, + electricInput: /^\s*[})\]]/, + fold: "brace", + lineComment: "#", + closeBrackets: { + pairs: '()[]{}""', + explode: "()[]{}" + } + }; +}, "graphqlModeFactory"); +o.CodeMirror.defineMode("graphql", l); + +/***/ }), + +/***/ "../../graphiql-react/dist/mode.cjs2.js": +/*!**********************************************!*\ + !*** ../../graphiql-react/dist/mode.cjs2.js ***! + \**********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +var n = Object.defineProperty; +var u = (t, r) => n(t, "name", { + value: r, + configurable: !0 +}); +const i = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + e = __webpack_require__(/*! graphql-language-service */ "../../graphql-language-service/esm/index.js"), + s = __webpack_require__(/*! ./mode-indent.cjs.js */ "../../graphiql-react/dist/mode-indent.cjs.js"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +i.CodeMirror.defineMode("graphql-variables", t => { + const r = e.onlineParser({ + eatWhitespace: a => a.eatSpace(), + lexRules: c, + parseRules: o, + editorConfig: { + tabSize: t.tabSize + } + }); + return { + config: t, + startState: r.startState, + token: r.token, + indent: s.indent, + electricInput: /^\s*[}\]]/, + fold: "brace", + closeBrackets: { + pairs: '[]{}""', + explode: "[]{}" + } + }; +}); +const c = { + Punctuation: /^\[|]|\{|\}|:|,/, + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + String: /^"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?/, + Keyword: /^true|false|null/ + }, + o = { + Document: [e.p("{"), e.list("Variable", e.opt(e.p(","))), e.p("}")], + Variable: [l("variable"), e.p(":"), "Value"], + Value(t) { + switch (t.kind) { + case "Number": + return "NumberValue"; + case "String": + return "StringValue"; + case "Punctuation": + switch (t.value) { + case "[": + return "ListValue"; + case "{": + return "ObjectValue"; + } + return null; + case "Keyword": + switch (t.value) { + case "true": + case "false": + return "BooleanValue"; + case "null": + return "NullValue"; + } + return null; + } + }, + NumberValue: [e.t("Number", "number")], + StringValue: [e.t("String", "string")], + BooleanValue: [e.t("Keyword", "builtin")], + NullValue: [e.t("Keyword", "keyword")], + ListValue: [e.p("["), e.list("Value", e.opt(e.p(","))), e.p("]")], + ObjectValue: [e.p("{"), e.list("ObjectField", e.opt(e.p(","))), e.p("}")], + ObjectField: [l("attribute"), e.p(":"), "Value"] + }; +function l(t) { + return { + style: t, + match: r => r.kind === "String", + update(r, a) { + r.name = a.value.slice(1, -1); + } + }; +} +u(l, "namedKey"); + +/***/ }), + +/***/ "../../graphiql-react/dist/mode.cjs3.js": +/*!**********************************************!*\ + !*** ../../graphiql-react/dist/mode.cjs3.js ***! + \**********************************************/ +/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { + + + +const a = __webpack_require__(/*! ./codemirror.cjs.js */ "../../graphiql-react/dist/codemirror.cjs.js"), + e = __webpack_require__(/*! graphql-language-service */ "../../graphql-language-service/esm/index.js"), + l = __webpack_require__(/*! ./mode-indent.cjs.js */ "../../graphiql-react/dist/mode-indent.cjs.js"); +__webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +a.CodeMirror.defineMode("graphql-results", r => { + const t = e.onlineParser({ + eatWhitespace: u => u.eatSpace(), + lexRules: n, + parseRules: s, + editorConfig: { + tabSize: r.tabSize + } + }); + return { + config: r, + startState: t.startState, + token: t.token, + indent: l.indent, + electricInput: /^\s*[}\]]/, + fold: "brace", + closeBrackets: { + pairs: '[]{}""', + explode: "[]{}" + } + }; +}); +const n = { + Punctuation: /^\[|]|\{|\}|:|,/, + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + String: /^"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?/, + Keyword: /^true|false|null/ + }, + s = { + Document: [e.p("{"), e.list("Entry", e.p(",")), e.p("}")], + Entry: [e.t("String", "def"), e.p(":"), "Value"], + Value(r) { + switch (r.kind) { + case "Number": + return "NumberValue"; + case "String": + return "StringValue"; + case "Punctuation": + switch (r.value) { + case "[": + return "ListValue"; + case "{": + return "ObjectValue"; + } + return null; + case "Keyword": + switch (r.value) { + case "true": + case "false": + return "BooleanValue"; + case "null": + return "NullValue"; + } + return null; + } + }, + NumberValue: [e.t("Number", "number")], + StringValue: [e.t("String", "string")], + BooleanValue: [e.t("Keyword", "builtin")], + NullValue: [e.t("Keyword", "keyword")], + ListValue: [e.p("["), e.list("Value", e.p(",")), e.p("]")], + ObjectValue: [e.p("{"), e.list("ObjectField", e.p(",")), e.p("}")], + ObjectField: [e.t("String", "property"), e.p(":"), "Value"] + }; + +/***/ }), + +/***/ "../../graphiql-react/dist/search.cjs.js": +/*!***********************************************!*\ + !*** ../../graphiql-react/dist/search.cjs.js ***! + \***********************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var K = Object.defineProperty; +var a = (S, O) => K(S, "name", { + value: O, + configurable: !0 +}); +const Q = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"), + L = __webpack_require__(/*! ./searchcursor.cjs2.js */ "../../graphiql-react/dist/searchcursor.cjs2.js"), + z = __webpack_require__(/*! ./dialog.cjs2.js */ "../../graphiql-react/dist/dialog.cjs2.js"); +function U(S, O) { + for (var i = 0; i < O.length; i++) { + const y = O[i]; + if (typeof y != "string" && !Array.isArray(y)) { + for (const v in y) if (v !== "default" && !(v in S)) { + const h = Object.getOwnPropertyDescriptor(y, v); + h && Object.defineProperty(S, v, h.get ? h : { + enumerable: !0, + get: () => y[v] + }); + } + } + } + return Object.freeze(Object.defineProperty(S, Symbol.toStringTag, { + value: "Module" + })); +} +a(U, "_mergeNamespaces"); +var B = { + exports: {} +}; +(function (S, O) { + (function (i) { + i(Q.requireCodemirror(), L.requireSearchcursor(), z.requireDialog()); + })(function (i) { + i.defineOption("search", { + bottom: !1 + }); + function y(e, n) { + return typeof e == "string" ? e = new RegExp(e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), n ? "gi" : "g") : e.global || (e = new RegExp(e.source, e.ignoreCase ? "gi" : "g")), { + token: function (t) { + e.lastIndex = t.pos; + var o = e.exec(t.string); + if (o && o.index == t.pos) return t.pos += o[0].length || 1, "searching"; + o ? t.pos = o.index : t.skipToEnd(); + } + }; + } + a(y, "searchOverlay"); + function v() { + this.posFrom = this.posTo = this.lastQuery = this.query = null, this.overlay = null; + } + a(v, "SearchState"); + function h(e) { + return e.state.search || (e.state.search = new v()); + } + a(h, "getSearchState"); + function m(e) { + return typeof e == "string" && e == e.toLowerCase(); + } + a(m, "queryCaseInsensitive"); + function N(e, n, t) { + return e.getSearchCursor(n, t, { + caseFold: m(n), + multiline: !0 + }); + } + a(N, "getSearchCursor"); + function j(e, n, t, o, r) { + e.openDialog(n, o, { + value: t, + selectValueOnOpen: !0, + closeOnEnter: !1, + onClose: function () { + w(e); + }, + onKeyDown: r, + bottom: e.options.search.bottom + }); + } + a(j, "persistentDialog"); + function R(e, n, t, o, r) { + e.openDialog ? e.openDialog(n, r, { + value: o, + selectValueOnOpen: !0, + bottom: e.options.search.bottom + }) : r(prompt(t, o)); + } + a(R, "dialog"); + function k(e, n, t, o) { + e.openConfirm ? e.openConfirm(n, o) : confirm(t) && o[0](); + } + a(k, "confirmDialog"); + function C(e) { + return e.replace(/\\([nrt\\])/g, function (n, t) { + return t == "n" ? ` +` : t == "r" ? "\r" : t == "t" ? " " : t == "\\" ? "\\" : n; + }); + } + a(C, "parseString"); + function T(e) { + var n = e.match(/^\/(.*)\/([a-z]*)$/); + if (n) try { + e = new RegExp(n[1], n[2].indexOf("i") == -1 ? "" : "i"); + } catch {} else e = C(e); + return (typeof e == "string" ? e == "" : e.test("")) && (e = /x^/), e; + } + a(T, "parseQuery"); + function D(e, n, t) { + n.queryText = t, n.query = T(t), e.removeOverlay(n.overlay, m(n.query)), n.overlay = y(n.query, m(n.query)), e.addOverlay(n.overlay), e.showMatchesOnScrollbar && (n.annotate && (n.annotate.clear(), n.annotate = null), n.annotate = e.showMatchesOnScrollbar(n.query, m(n.query))); + } + a(D, "startSearch"); + function b(e, n, t, o) { + var r = h(e); + if (r.query) return P(e, n); + var s = e.getSelection() || r.lastQuery; + if (s instanceof RegExp && s.source == "x^" && (s = null), t && e.openDialog) { + var c = null, + u = a(function (f, x) { + i.e_stop(x), f && (f != r.queryText && (D(e, r, f), r.posFrom = r.posTo = e.getCursor()), c && (c.style.opacity = 1), P(e, x.shiftKey, function (d, g) { + var p; + g.line < 3 && document.querySelector && (p = e.display.wrapper.querySelector(".CodeMirror-dialog")) && p.getBoundingClientRect().bottom - 4 > e.cursorCoords(g, "window").top && ((c = p).style.opacity = .4); + })); + }, "searchNext"); + j(e, _(e), s, u, function (f, x) { + var d = i.keyName(f), + g = e.getOption("extraKeys"), + p = g && g[d] || i.keyMap[e.getOption("keyMap")][d]; + p == "findNext" || p == "findPrev" || p == "findPersistentNext" || p == "findPersistentPrev" ? (i.e_stop(f), D(e, h(e), x), e.execCommand(p)) : (p == "find" || p == "findPersistent") && (i.e_stop(f), u(x, f)); + }), o && s && (D(e, r, s), P(e, n)); + } else R(e, _(e), "Search for:", s, function (f) { + f && !r.query && e.operation(function () { + D(e, r, f), r.posFrom = r.posTo = e.getCursor(), P(e, n); + }); + }); + } + a(b, "doSearch"); + function P(e, n, t) { + e.operation(function () { + var o = h(e), + r = N(e, o.query, n ? o.posFrom : o.posTo); + !r.find(n) && (r = N(e, o.query, n ? i.Pos(e.lastLine()) : i.Pos(e.firstLine(), 0)), !r.find(n)) || (e.setSelection(r.from(), r.to()), e.scrollIntoView({ + from: r.from(), + to: r.to() + }, 20), o.posFrom = r.from(), o.posTo = r.to(), t && t(r.from(), r.to())); + }); + } + a(P, "findNext"); + function w(e) { + e.operation(function () { + var n = h(e); + n.lastQuery = n.query, n.query && (n.query = n.queryText = null, e.removeOverlay(n.overlay), n.annotate && (n.annotate.clear(), n.annotate = null)); + }); + } + a(w, "clearSearch"); + function l(e, n) { + var t = e ? document.createElement(e) : document.createDocumentFragment(); + for (var o in n) t[o] = n[o]; + for (var r = 2; r < arguments.length; r++) { + var s = arguments[r]; + t.appendChild(typeof s == "string" ? document.createTextNode(s) : s); + } + return t; + } + a(l, "el"); + function _(e) { + return l("", null, l("span", { + className: "CodeMirror-search-label" + }, e.phrase("Search:")), " ", l("input", { + type: "text", + style: "width: 10em", + className: "CodeMirror-search-field" + }), " ", l("span", { + style: "color: #888", + className: "CodeMirror-search-hint" + }, e.phrase("(Use /re/ syntax for regexp search)"))); + } + a(_, "getQueryDialog"); + function A(e) { + return l("", null, " ", l("input", { + type: "text", + style: "width: 10em", + className: "CodeMirror-search-field" + }), " ", l("span", { + style: "color: #888", + className: "CodeMirror-search-hint" + }, e.phrase("(Use /re/ syntax for regexp search)"))); + } + a(A, "getReplaceQueryDialog"); + function I(e) { + return l("", null, l("span", { + className: "CodeMirror-search-label" + }, e.phrase("With:")), " ", l("input", { + type: "text", + style: "width: 10em", + className: "CodeMirror-search-field" + })); + } + a(I, "getReplacementQueryDialog"); + function V(e) { + return l("", null, l("span", { + className: "CodeMirror-search-label" + }, e.phrase("Replace?")), " ", l("button", {}, e.phrase("Yes")), " ", l("button", {}, e.phrase("No")), " ", l("button", {}, e.phrase("All")), " ", l("button", {}, e.phrase("Stop"))); + } + a(V, "getDoReplaceConfirm"); + function E(e, n, t) { + e.operation(function () { + for (var o = N(e, n); o.findNext();) if (typeof n != "string") { + var r = e.getRange(o.from(), o.to()).match(n); + o.replace(t.replace(/\$(\d)/g, function (s, c) { + return r[c]; + })); + } else o.replace(t); + }); + } + a(E, "replaceAll"); + function F(e, n) { + if (!e.getOption("readOnly")) { + var t = e.getSelection() || h(e).lastQuery, + o = n ? e.phrase("Replace all:") : e.phrase("Replace:"), + r = l("", null, l("span", { + className: "CodeMirror-search-label" + }, o), A(e)); + R(e, r, o, t, function (s) { + s && (s = T(s), R(e, I(e), e.phrase("Replace with:"), "", function (c) { + if (c = C(c), n) E(e, s, c);else { + w(e); + var u = N(e, s, e.getCursor("from")), + f = a(function () { + var d = u.from(), + g; + !(g = u.findNext()) && (u = N(e, s), !(g = u.findNext()) || d && u.from().line == d.line && u.from().ch == d.ch) || (e.setSelection(u.from(), u.to()), e.scrollIntoView({ + from: u.from(), + to: u.to() + }), k(e, V(e), e.phrase("Replace?"), [function () { + x(g); + }, f, function () { + E(e, s, c); + }])); + }, "advance"), + x = a(function (d) { + u.replace(typeof s == "string" ? c : c.replace(/\$(\d)/g, function (g, p) { + return d[p]; + })), f(); + }, "doReplace"); + f(); + } + })); + }); + } + } + a(F, "replace"), i.commands.find = function (e) { + w(e), b(e); + }, i.commands.findPersistent = function (e) { + w(e), b(e, !1, !0); + }, i.commands.findPersistentNext = function (e) { + b(e, !1, !0, !0); + }, i.commands.findPersistentPrev = function (e) { + b(e, !0, !0, !0); + }, i.commands.findNext = b, i.commands.findPrev = function (e) { + b(e, !0); + }, i.commands.clearSearch = w, i.commands.replace = F, i.commands.replaceAll = function (e) { + F(e, !0); + }; + }); +})(); +var $ = B.exports; +const W = Q.getDefaultExportFromCjs($), + Y = U({ + __proto__: null, + default: W + }, [$]); +exports.search = Y; + +/***/ }), + +/***/ "../../graphiql-react/dist/searchcursor.cjs.js": +/*!*****************************************************!*\ + !*** ../../graphiql-react/dist/searchcursor.cjs.js ***! + \*****************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var n = Object.defineProperty; +var u = (r, o) => n(r, "name", { + value: o, + configurable: !0 +}); +const i = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"), + f = __webpack_require__(/*! ./searchcursor.cjs2.js */ "../../graphiql-react/dist/searchcursor.cjs2.js"); +function l(r, o) { + for (var c = 0; c < o.length; c++) { + const e = o[c]; + if (typeof e != "string" && !Array.isArray(e)) { + for (const t in e) if (t !== "default" && !(t in r)) { + const s = Object.getOwnPropertyDescriptor(e, t); + s && Object.defineProperty(r, t, s.get ? s : { + enumerable: !0, + get: () => e[t] + }); + } + } + } + return Object.freeze(Object.defineProperty(r, Symbol.toStringTag, { + value: "Module" + })); +} +u(l, "_mergeNamespaces"); +var a = f.requireSearchcursor(); +const g = i.getDefaultExportFromCjs(a), + p = l({ + __proto__: null, + default: g + }, [a]); +exports.searchcursor = p; + +/***/ }), + +/***/ "../../graphiql-react/dist/searchcursor.cjs2.js": +/*!******************************************************!*\ + !*** ../../graphiql-react/dist/searchcursor.cjs2.js ***! + \******************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var W = Object.defineProperty; +var o = (d, E) => W(d, "name", { + value: E, + configurable: !0 +}); +const G = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +var N = { + exports: {} + }, + b; +function H() { + return b || (b = 1, function (d, E) { + (function (m) { + m(G.requireCodemirror()); + })(function (m) { + var a = m.Pos; + function B(e) { + var t = e.flags; + return t !== null && t !== void 0 ? t : (e.ignoreCase ? "i" : "") + (e.global ? "g" : "") + (e.multiline ? "m" : ""); + } + o(B, "regexpFlags"); + function F(e, t) { + for (var n = B(e), r = n, l = 0; l < t.length; l++) r.indexOf(t.charAt(l)) == -1 && (r += t.charAt(l)); + return n == r ? e : new RegExp(e.source, r); + } + o(F, "ensureFlags"); + function R(e) { + return /\\s|\\n|\n|\\W|\\D|\[\^/.test(e.source); + } + o(R, "maybeMultiline"); + function I(e, t, n) { + t = F(t, "g"); + for (var r = n.line, l = n.ch, i = e.lastLine(); r <= i; r++, l = 0) { + t.lastIndex = l; + var h = e.getLine(r), + f = t.exec(h); + if (f) return { + from: a(r, f.index), + to: a(r, f.index + f[0].length), + match: f + }; + } + } + o(I, "searchRegexpForward"); + function j(e, t, n) { + if (!R(t)) return I(e, t, n); + t = F(t, "gm"); + for (var r, l = 1, i = n.line, h = e.lastLine(); i <= h;) { + for (var f = 0; f < l && !(i > h); f++) { + var p = e.getLine(i++); + r = r == null ? p : r + ` +` + p; + } + l = l * 2, t.lastIndex = n.ch; + var u = t.exec(r); + if (u) { + var s = r.slice(0, u.index).split(` +`), + c = u[0].split(` +`), + g = n.line + s.length - 1, + v = s[s.length - 1].length; + return { + from: a(g, v), + to: a(g + c.length - 1, c.length == 1 ? v + c[0].length : c[c.length - 1].length), + match: u + }; + } + } + } + o(j, "searchRegexpForwardMultiline"); + function z(e, t, n) { + for (var r, l = 0; l <= e.length;) { + t.lastIndex = l; + var i = t.exec(e); + if (!i) break; + var h = i.index + i[0].length; + if (h > e.length - n) break; + (!r || h > r.index + r[0].length) && (r = i), l = i.index + 1; + } + return r; + } + o(z, "lastMatchIn"); + function D(e, t, n) { + t = F(t, "g"); + for (var r = n.line, l = n.ch, i = e.firstLine(); r >= i; r--, l = -1) { + var h = e.getLine(r), + f = z(h, t, l < 0 ? 0 : h.length - l); + if (f) return { + from: a(r, f.index), + to: a(r, f.index + f[0].length), + match: f + }; + } + } + o(D, "searchRegexpBackward"); + function A(e, t, n) { + if (!R(t)) return D(e, t, n); + t = F(t, "gm"); + for (var r, l = 1, i = e.getLine(n.line).length - n.ch, h = n.line, f = e.firstLine(); h >= f;) { + for (var p = 0; p < l && h >= f; p++) { + var u = e.getLine(h--); + r = r == null ? u : u + ` +` + r; + } + l *= 2; + var s = z(r, t, i); + if (s) { + var c = r.slice(0, s.index).split(` +`), + g = s[0].split(` +`), + v = h + c.length, + x = c[c.length - 1].length; + return { + from: a(v, x), + to: a(v + g.length - 1, g.length == 1 ? x + g[0].length : g[g.length - 1].length), + match: s + }; + } + } + } + o(A, "searchRegexpBackwardMultiline"); + var P, k; + String.prototype.normalize ? (P = o(function (e) { + return e.normalize("NFD").toLowerCase(); + }, "doFold"), k = o(function (e) { + return e.normalize("NFD"); + }, "noFold")) : (P = o(function (e) { + return e.toLowerCase(); + }, "doFold"), k = o(function (e) { + return e; + }, "noFold")); + function L(e, t, n, r) { + if (e.length == t.length) return n; + for (var l = 0, i = n + Math.max(0, e.length - t.length);;) { + if (l == i) return l; + var h = l + i >> 1, + f = r(e.slice(0, h)).length; + if (f == n) return h; + f > n ? i = h : l = h + 1; + } + } + o(L, "adjustPos"); + function y(e, t, n, r) { + if (!t.length) return null; + var l = r ? P : k, + i = l(t).split(/\r|\n\r?/); + t: for (var h = n.line, f = n.ch, p = e.lastLine() + 1 - i.length; h <= p; h++, f = 0) { + var u = e.getLine(h).slice(f), + s = l(u); + if (i.length == 1) { + var c = s.indexOf(i[0]); + if (c == -1) continue t; + var n = L(u, s, c, l) + f; + return { + from: a(h, L(u, s, c, l) + f), + to: a(h, L(u, s, c + i[0].length, l) + f) + }; + } else { + var g = s.length - i[0].length; + if (s.slice(g) != i[0]) continue t; + for (var v = 1; v < i.length - 1; v++) if (l(e.getLine(h + v)) != i[v]) continue t; + var x = e.getLine(h + i.length - 1), + O = l(x), + S = i[i.length - 1]; + if (O.slice(0, S.length) != S) continue t; + return { + from: a(h, L(u, s, g, l) + f), + to: a(h + i.length - 1, L(x, O, S.length, l)) + }; + } + } + } + o(y, "searchStringForward"); + function C(e, t, n, r) { + if (!t.length) return null; + var l = r ? P : k, + i = l(t).split(/\r|\n\r?/); + t: for (var h = n.line, f = n.ch, p = e.firstLine() - 1 + i.length; h >= p; h--, f = -1) { + var u = e.getLine(h); + f > -1 && (u = u.slice(0, f)); + var s = l(u); + if (i.length == 1) { + var c = s.lastIndexOf(i[0]); + if (c == -1) continue t; + return { + from: a(h, L(u, s, c, l)), + to: a(h, L(u, s, c + i[0].length, l)) + }; + } else { + var g = i[i.length - 1]; + if (s.slice(0, g.length) != g) continue t; + for (var v = 1, n = h - i.length + 1; v < i.length - 1; v++) if (l(e.getLine(n + v)) != i[v]) continue t; + var x = e.getLine(h + 1 - i.length), + O = l(x); + if (O.slice(O.length - i[0].length) != i[0]) continue t; + return { + from: a(h + 1 - i.length, L(x, O, x.length - i[0].length, l)), + to: a(h, L(u, s, g.length, l)) + }; + } + } + } + o(C, "searchStringBackward"); + function w(e, t, n, r) { + this.atOccurrence = !1, this.afterEmptyMatch = !1, this.doc = e, n = n ? e.clipPos(n) : a(0, 0), this.pos = { + from: n, + to: n + }; + var l; + typeof r == "object" ? l = r.caseFold : (l = r, r = null), typeof t == "string" ? (l == null && (l = !1), this.matches = function (i, h) { + return (i ? C : y)(e, t, h, l); + }) : (t = F(t, "gm"), !r || r.multiline !== !1 ? this.matches = function (i, h) { + return (i ? A : j)(e, t, h); + } : this.matches = function (i, h) { + return (i ? D : I)(e, t, h); + }); + } + o(w, "SearchCursor"), w.prototype = { + findNext: function () { + return this.find(!1); + }, + findPrevious: function () { + return this.find(!0); + }, + find: function (e) { + var t = this.doc.clipPos(e ? this.pos.from : this.pos.to); + if (this.afterEmptyMatch && this.atOccurrence && (t = a(t.line, t.ch), e ? (t.ch--, t.ch < 0 && (t.line--, t.ch = (this.doc.getLine(t.line) || "").length)) : (t.ch++, t.ch > (this.doc.getLine(t.line) || "").length && (t.ch = 0, t.line++)), m.cmpPos(t, this.doc.clipPos(t)) != 0)) return this.atOccurrence = !1; + var n = this.matches(e, t); + if (this.afterEmptyMatch = n && m.cmpPos(n.from, n.to) == 0, n) return this.pos = n, this.atOccurrence = !0, this.pos.match || !0; + var r = a(e ? this.doc.firstLine() : this.doc.lastLine() + 1, 0); + return this.pos = { + from: r, + to: r + }, this.atOccurrence = !1; + }, + from: function () { + if (this.atOccurrence) return this.pos.from; + }, + to: function () { + if (this.atOccurrence) return this.pos.to; + }, + replace: function (e, t) { + if (this.atOccurrence) { + var n = m.splitLines(e); + this.doc.replaceRange(n, this.pos.from, this.pos.to, t), this.pos.to = a(this.pos.from.line + n.length - 1, n[n.length - 1].length + (n.length == 1 ? this.pos.from.ch : 0)); + } + } + }, m.defineExtension("getSearchCursor", function (e, t, n) { + return new w(this.doc, e, t, n); + }), m.defineDocExtension("getSearchCursor", function (e, t, n) { + return new w(this, e, t, n); + }), m.defineExtension("selectMatches", function (e, t) { + for (var n = [], r = this.getSearchCursor(e, this.getCursor("from"), t); r.findNext() && !(m.cmpPos(r.to(), this.getCursor("to")) > 0);) n.push({ + anchor: r.from(), + head: r.to() + }); + n.length && this.setSelections(n, 0); + }); + }); + }()), N.exports; +} +o(H, "requireSearchcursor"); +exports.requireSearchcursor = H; + +/***/ }), + +/***/ "../../graphiql-react/dist/show-hint.cjs.js": +/*!**************************************************!*\ + !*** ../../graphiql-react/dist/show-hint.cjs.js ***! + \**************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var ct = Object.defineProperty; +var p = (H, A) => ct(H, "name", { + value: A, + configurable: !0 +}); +const G = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"); +function lt(H, A) { + for (var r = 0; r < A.length; r++) { + const w = A[r]; + if (typeof w != "string" && !Array.isArray(w)) { + for (const v in w) if (v !== "default" && !(v in H)) { + const b = Object.getOwnPropertyDescriptor(w, v); + b && Object.defineProperty(H, v, b.get ? b : { + enumerable: !0, + get: () => w[v] + }); + } + } + } + return Object.freeze(Object.defineProperty(H, Symbol.toStringTag, { + value: "Module" + })); +} +p(lt, "_mergeNamespaces"); +var ht = { + exports: {} +}; +(function (H, A) { + (function (r) { + r(G.requireCodemirror()); + })(function (r) { + var w = "CodeMirror-hint", + v = "CodeMirror-hint-active"; + r.showHint = function (t, e, i) { + if (!e) return t.showHint(i); + i && i.async && (e.async = !0); + var n = { + hint: e + }; + if (i) for (var s in i) n[s] = i[s]; + return t.showHint(n); + }, r.defineExtension("showHint", function (t) { + t = tt(this, this.getCursor("start"), t); + var e = this.listSelections(); + if (!(e.length > 1)) { + if (this.somethingSelected()) { + if (!t.hint.supportsSelection) return; + for (var i = 0; i < e.length; i++) if (e[i].head.line != e[i].anchor.line) return; + } + this.state.completionActive && this.state.completionActive.close(); + var n = this.state.completionActive = new b(this, t); + n.options.hint && (r.signal(this, "startCompletion", this), n.update(!0)); + } + }), r.defineExtension("closeHint", function () { + this.state.completionActive && this.state.completionActive.close(); + }); + function b(t, e) { + if (this.cm = t, this.options = e, this.widget = null, this.debounce = 0, this.tick = 0, this.startPos = this.cm.getCursor("start"), this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length, this.options.updateOnCursorActivity) { + var i = this; + t.on("cursorActivity", this.activityFunc = function () { + i.cursorActivity(); + }); + } + } + p(b, "Completion"); + var Q = window.requestAnimationFrame || function (t) { + return setTimeout(t, 1e3 / 60); + }, + Z = window.cancelAnimationFrame || clearTimeout; + b.prototype = { + close: function () { + this.active() && (this.cm.state.completionActive = null, this.tick = null, this.options.updateOnCursorActivity && this.cm.off("cursorActivity", this.activityFunc), this.widget && this.data && r.signal(this.data, "close"), this.widget && this.widget.close(), r.signal(this.cm, "endCompletion", this.cm)); + }, + active: function () { + return this.cm.state.completionActive == this; + }, + pick: function (t, e) { + var i = t.list[e], + n = this; + this.cm.operation(function () { + i.hint ? i.hint(n.cm, t, i) : n.cm.replaceRange(_(i), i.from || t.from, i.to || t.to, "complete"), r.signal(t, "pick", i), n.cm.scrollIntoView(); + }), this.options.closeOnPick && this.close(); + }, + cursorActivity: function () { + this.debounce && (Z(this.debounce), this.debounce = 0); + var t = this.startPos; + this.data && (t = this.data.from); + var e = this.cm.getCursor(), + i = this.cm.getLine(e.line); + if (e.line != this.startPos.line || i.length - e.ch != this.startLen - this.startPos.ch || e.ch < t.ch || this.cm.somethingSelected() || !e.ch || this.options.closeCharacters.test(i.charAt(e.ch - 1))) this.close();else { + var n = this; + this.debounce = Q(function () { + n.update(); + }), this.widget && this.widget.disable(); + } + }, + update: function (t) { + if (this.tick != null) { + var e = this, + i = ++this.tick; + U(this.options.hint, this.cm, this.options, function (n) { + e.tick == i && e.finishUpdate(n, t); + }); + } + }, + finishUpdate: function (t, e) { + this.data && r.signal(this.data, "update"); + var i = this.widget && this.widget.picked || e && this.options.completeSingle; + this.widget && this.widget.close(), this.data = t, t && t.list.length && (i && t.list.length == 1 ? this.pick(t, 0) : (this.widget = new K(this, t), r.signal(t, "shown"))); + } + }; + function tt(t, e, i) { + var n = t.options.hintOptions, + s = {}; + for (var c in D) s[c] = D[c]; + if (n) for (var c in n) n[c] !== void 0 && (s[c] = n[c]); + if (i) for (var c in i) i[c] !== void 0 && (s[c] = i[c]); + return s.hint.resolve && (s.hint = s.hint.resolve(t, e)), s; + } + p(tt, "parseOptions"); + function _(t) { + return typeof t == "string" ? t : t.text; + } + p(_, "getText"); + function et(t, e) { + var i = { + Up: function () { + e.moveFocus(-1); + }, + Down: function () { + e.moveFocus(1); + }, + PageUp: function () { + e.moveFocus(-e.menuSize() + 1, !0); + }, + PageDown: function () { + e.moveFocus(e.menuSize() - 1, !0); + }, + Home: function () { + e.setFocus(0); + }, + End: function () { + e.setFocus(e.length - 1); + }, + Enter: e.pick, + Tab: e.pick, + Esc: e.close + }, + n = /Mac/.test(navigator.platform); + n && (i["Ctrl-P"] = function () { + e.moveFocus(-1); + }, i["Ctrl-N"] = function () { + e.moveFocus(1); + }); + var s = t.options.customKeys, + c = s ? {} : i; + function o(u, l) { + var a; + typeof l != "string" ? a = p(function (S) { + return l(S, e); + }, "bound") : i.hasOwnProperty(l) ? a = i[l] : a = l, c[u] = a; + } + if (p(o, "addBinding"), s) for (var f in s) s.hasOwnProperty(f) && o(f, s[f]); + var h = t.options.extraKeys; + if (h) for (var f in h) h.hasOwnProperty(f) && o(f, h[f]); + return c; + } + p(et, "buildKeyMap"); + function B(t, e) { + for (; e && e != t;) { + if (e.nodeName.toUpperCase() === "LI" && e.parentNode == t) return e; + e = e.parentNode; + } + } + p(B, "getHintElement"); + function K(t, e) { + this.id = "cm-complete-" + Math.floor(Math.random(1e6)), this.completion = t, this.data = e, this.picked = !1; + var i = this, + n = t.cm, + s = n.getInputField().ownerDocument, + c = s.defaultView || s.parentWindow, + o = this.hints = s.createElement("ul"); + o.setAttribute("role", "listbox"), o.setAttribute("aria-expanded", "true"), o.id = this.id; + var f = t.cm.options.theme; + o.className = "CodeMirror-hints " + f, this.selectedHint = e.selectedHint || 0; + for (var h = e.list, u = 0; u < h.length; ++u) { + var l = o.appendChild(s.createElement("li")), + a = h[u], + S = w + (u != this.selectedHint ? "" : " " + v); + a.className != null && (S = a.className + " " + S), l.className = S, u == this.selectedHint && l.setAttribute("aria-selected", "true"), l.id = this.id + "-" + u, l.setAttribute("role", "option"), a.render ? a.render(l, e, a) : l.appendChild(s.createTextNode(a.displayText || _(a))), l.hintId = u; + } + var T = t.options.container || s.body, + y = n.cursorCoords(t.options.alignWithWord ? e.from : null), + k = y.left, + O = y.bottom, + j = !0, + F = 0, + E = 0; + if (T !== s.body) { + var st = ["absolute", "relative", "fixed"].indexOf(c.getComputedStyle(T).position) !== -1, + W = st ? T : T.offsetParent, + M = W.getBoundingClientRect(), + q = s.body.getBoundingClientRect(); + F = M.left - q.left - W.scrollLeft, E = M.top - q.top - W.scrollTop; + } + o.style.left = k - F + "px", o.style.top = O - E + "px"; + var N = c.innerWidth || Math.max(s.body.offsetWidth, s.documentElement.offsetWidth), + L = c.innerHeight || Math.max(s.body.offsetHeight, s.documentElement.offsetHeight); + T.appendChild(o), n.getInputField().setAttribute("aria-autocomplete", "list"), n.getInputField().setAttribute("aria-owns", this.id), n.getInputField().setAttribute("aria-activedescendant", this.id + "-" + this.selectedHint); + var m = t.options.moveOnOverlap ? o.getBoundingClientRect() : new DOMRect(), + z = t.options.paddingForScrollbar ? o.scrollHeight > o.clientHeight + 1 : !1, + x; + setTimeout(function () { + x = n.getScrollInfo(); + }); + var ot = m.bottom - L; + if (ot > 0) { + var P = m.bottom - m.top, + rt = y.top - (y.bottom - m.top); + if (rt - P > 0) o.style.top = (O = y.top - P - E) + "px", j = !1;else if (P > L) { + o.style.height = L - 5 + "px", o.style.top = (O = y.bottom - m.top - E) + "px"; + var V = n.getCursor(); + e.from.ch != V.ch && (y = n.cursorCoords(V), o.style.left = (k = y.left - F) + "px", m = o.getBoundingClientRect()); + } + } + var C = m.right - N; + if (z && (C += n.display.nativeBarWidth), C > 0 && (m.right - m.left > N && (o.style.width = N - 5 + "px", C -= m.right - m.left - N), o.style.left = (k = y.left - C - F) + "px"), z) for (var I = o.firstChild; I; I = I.nextSibling) I.style.paddingRight = n.display.nativeBarWidth + "px"; + if (n.addKeyMap(this.keyMap = et(t, { + moveFocus: function (d, g) { + i.changeActive(i.selectedHint + d, g); + }, + setFocus: function (d) { + i.changeActive(d); + }, + menuSize: function () { + return i.screenAmount(); + }, + length: h.length, + close: function () { + t.close(); + }, + pick: function () { + i.pick(); + }, + data: e + })), t.options.closeOnUnfocus) { + var Y; + n.on("blur", this.onBlur = function () { + Y = setTimeout(function () { + t.close(); + }, 100); + }), n.on("focus", this.onFocus = function () { + clearTimeout(Y); + }); + } + n.on("scroll", this.onScroll = function () { + var d = n.getScrollInfo(), + g = n.getWrapperElement().getBoundingClientRect(); + x || (x = n.getScrollInfo()); + var X = O + x.top - d.top, + R = X - (c.pageYOffset || (s.documentElement || s.body).scrollTop); + if (j || (R += o.offsetHeight), R <= g.top || R >= g.bottom) return t.close(); + o.style.top = X + "px", o.style.left = k + x.left - d.left + "px"; + }), r.on(o, "dblclick", function (d) { + var g = B(o, d.target || d.srcElement); + g && g.hintId != null && (i.changeActive(g.hintId), i.pick()); + }), r.on(o, "click", function (d) { + var g = B(o, d.target || d.srcElement); + g && g.hintId != null && (i.changeActive(g.hintId), t.options.completeOnSingleClick && i.pick()); + }), r.on(o, "mousedown", function () { + setTimeout(function () { + n.focus(); + }, 20); + }); + var $ = this.getSelectedHintRange(); + return ($.from !== 0 || $.to !== 0) && this.scrollToActive(), r.signal(e, "select", h[this.selectedHint], o.childNodes[this.selectedHint]), !0; + } + p(K, "Widget"), K.prototype = { + close: function () { + if (this.completion.widget == this) { + this.completion.widget = null, this.hints.parentNode && this.hints.parentNode.removeChild(this.hints), this.completion.cm.removeKeyMap(this.keyMap); + var t = this.completion.cm.getInputField(); + t.removeAttribute("aria-activedescendant"), t.removeAttribute("aria-owns"); + var e = this.completion.cm; + this.completion.options.closeOnUnfocus && (e.off("blur", this.onBlur), e.off("focus", this.onFocus)), e.off("scroll", this.onScroll); + } + }, + disable: function () { + this.completion.cm.removeKeyMap(this.keyMap); + var t = this; + this.keyMap = { + Enter: function () { + t.picked = !0; + } + }, this.completion.cm.addKeyMap(this.keyMap); + }, + pick: function () { + this.completion.pick(this.data, this.selectedHint); + }, + changeActive: function (t, e) { + if (t >= this.data.list.length ? t = e ? this.data.list.length - 1 : 0 : t < 0 && (t = e ? 0 : this.data.list.length - 1), this.selectedHint != t) { + var i = this.hints.childNodes[this.selectedHint]; + i && (i.className = i.className.replace(" " + v, ""), i.removeAttribute("aria-selected")), i = this.hints.childNodes[this.selectedHint = t], i.className += " " + v, i.setAttribute("aria-selected", "true"), this.completion.cm.getInputField().setAttribute("aria-activedescendant", i.id), this.scrollToActive(), r.signal(this.data, "select", this.data.list[this.selectedHint], i); + } + }, + scrollToActive: function () { + var t = this.getSelectedHintRange(), + e = this.hints.childNodes[t.from], + i = this.hints.childNodes[t.to], + n = this.hints.firstChild; + e.offsetTop < this.hints.scrollTop ? this.hints.scrollTop = e.offsetTop - n.offsetTop : i.offsetTop + i.offsetHeight > this.hints.scrollTop + this.hints.clientHeight && (this.hints.scrollTop = i.offsetTop + i.offsetHeight - this.hints.clientHeight + n.offsetTop); + }, + screenAmount: function () { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + }, + getSelectedHintRange: function () { + var t = this.completion.options.scrollMargin || 0; + return { + from: Math.max(0, this.selectedHint - t), + to: Math.min(this.data.list.length - 1, this.selectedHint + t) + }; + } + }; + function it(t, e) { + if (!t.somethingSelected()) return e; + for (var i = [], n = 0; n < e.length; n++) e[n].supportsSelection && i.push(e[n]); + return i; + } + p(it, "applicableHelpers"); + function U(t, e, i, n) { + if (t.async) t(e, n, i);else { + var s = t(e, i); + s && s.then ? s.then(n) : n(s); + } + } + p(U, "fetchHints"); + function nt(t, e) { + var i = t.getHelpers(e, "hint"), + n; + if (i.length) { + var s = p(function (c, o, f) { + var h = it(c, i); + function u(l) { + if (l == h.length) return o(null); + U(h[l], c, f, function (a) { + a && a.list.length > 0 ? o(a) : u(l + 1); + }); + } + p(u, "run"), u(0); + }, "resolved"); + return s.async = !0, s.supportsSelection = !0, s; + } else return (n = t.getHelper(t.getCursor(), "hintWords")) ? function (c) { + return r.hint.fromList(c, { + words: n + }); + } : r.hint.anyword ? function (c, o) { + return r.hint.anyword(c, o); + } : function () {}; + } + p(nt, "resolveAutoHints"), r.registerHelper("hint", "auto", { + resolve: nt + }), r.registerHelper("hint", "fromList", function (t, e) { + var i = t.getCursor(), + n = t.getTokenAt(i), + s, + c = r.Pos(i.line, n.start), + o = i; + n.start < i.ch && /\w/.test(n.string.charAt(i.ch - n.start - 1)) ? s = n.string.substr(0, i.ch - n.start) : (s = "", c = i); + for (var f = [], h = 0; h < e.words.length; h++) { + var u = e.words[h]; + u.slice(0, s.length) == s && f.push(u); + } + if (f.length) return { + list: f, + from: c, + to: o + }; + }), r.commands.autocomplete = r.showHint; + var D = { + hint: r.hint.auto, + completeSingle: !0, + alignWithWord: !0, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnPick: !0, + closeOnUnfocus: !0, + updateOnCursorActivity: !0, + completeOnSingleClick: !0, + container: null, + customKeys: null, + extraKeys: null, + paddingForScrollbar: !0, + moveOnOverlap: !0 + }; + r.defineOption("hintOptions", null); + }); +})(); +var J = ht.exports; +const at = G.getDefaultExportFromCjs(J), + ft = lt({ + __proto__: null, + default: at + }, [J]); +exports.showHint = ft; + +/***/ }), + +/***/ "../../graphiql-react/dist/sublime.cjs.js": +/*!************************************************!*\ + !*** ../../graphiql-react/dist/sublime.cjs.js ***! + \************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +var _ = Object.defineProperty; +var v = (m, B) => _(m, "name", { + value: B, + configurable: !0 +}); +const E = __webpack_require__(/*! ./codemirror.cjs2.js */ "../../graphiql-react/dist/codemirror.cjs2.js"), + Y = __webpack_require__(/*! ./searchcursor.cjs2.js */ "../../graphiql-react/dist/searchcursor.cjs2.js"), + z = __webpack_require__(/*! ./matchbrackets.cjs2.js */ "../../graphiql-react/dist/matchbrackets.cjs2.js"); +function J(m, B) { + for (var h = 0; h < B.length; h++) { + const a = B[h]; + if (typeof a != "string" && !Array.isArray(a)) { + for (const f in a) if (f !== "default" && !(f in m)) { + const A = Object.getOwnPropertyDescriptor(a, f); + A && Object.defineProperty(m, f, A.get ? A : { + enumerable: !0, + get: () => a[f] + }); + } + } + } + return Object.freeze(Object.defineProperty(m, Symbol.toStringTag, { + value: "Module" + })); +} +v(J, "_mergeNamespaces"); +var G = { + exports: {} +}; +(function (m, B) { + (function (h) { + h(E.requireCodemirror(), Y.requireSearchcursor(), z.requireMatchbrackets()); + })(function (h) { + var a = h.commands, + f = h.Pos; + function A(e, t, n) { + if (n < 0 && t.ch == 0) return e.clipPos(f(t.line - 1)); + var r = e.getLine(t.line); + if (n > 0 && t.ch >= r.length) return e.clipPos(f(t.line + 1, 0)); + for (var l = "start", i, o = t.ch, s = o, u = n < 0 ? 0 : r.length, d = 0; s != u; s += n, d++) { + var p = r.charAt(n < 0 ? s - 1 : s), + c = p != "_" && h.isWordChar(p) ? "w" : "o"; + if (c == "w" && p.toUpperCase() == p && (c = "W"), l == "start") c != "o" ? (l = "in", i = c) : o = s + n;else if (l == "in" && i != c) { + if (i == "w" && c == "W" && n < 0 && s--, i == "W" && c == "w" && n > 0) if (s == o + 1) { + i = "w"; + continue; + } else s--; + break; + } + } + return f(t.line, s); + } + v(A, "findPosSubword"); + function T(e, t) { + e.extendSelectionsBy(function (n) { + return e.display.shift || e.doc.extend || n.empty() ? A(e.doc, n.head, t) : t < 0 ? n.from() : n.to(); + }); + } + v(T, "moveSubword"), a.goSubwordLeft = function (e) { + T(e, -1); + }, a.goSubwordRight = function (e) { + T(e, 1); + }, a.scrollLineUp = function (e) { + var t = e.getScrollInfo(); + if (!e.somethingSelected()) { + var n = e.lineAtHeight(t.top + t.clientHeight, "local"); + e.getCursor().line >= n && e.execCommand("goLineUp"); + } + e.scrollTo(null, t.top - e.defaultTextHeight()); + }, a.scrollLineDown = function (e) { + var t = e.getScrollInfo(); + if (!e.somethingSelected()) { + var n = e.lineAtHeight(t.top, "local") + 1; + e.getCursor().line <= n && e.execCommand("goLineDown"); + } + e.scrollTo(null, t.top + e.defaultTextHeight()); + }, a.splitSelectionByLine = function (e) { + for (var t = e.listSelections(), n = [], r = 0; r < t.length; r++) for (var l = t[r].from(), i = t[r].to(), o = l.line; o <= i.line; ++o) i.line > l.line && o == i.line && i.ch == 0 || n.push({ + anchor: o == l.line ? l : f(o, 0), + head: o == i.line ? i : f(o) + }); + e.setSelections(n, 0); + }, a.singleSelectionTop = function (e) { + var t = e.listSelections()[0]; + e.setSelection(t.anchor, t.head, { + scroll: !1 + }); + }, a.selectLine = function (e) { + for (var t = e.listSelections(), n = [], r = 0; r < t.length; r++) { + var l = t[r]; + n.push({ + anchor: f(l.from().line, 0), + head: f(l.to().line + 1, 0) + }); + } + e.setSelections(n); + }; + function x(e, t) { + if (e.isReadOnly()) return h.Pass; + e.operation(function () { + for (var n = e.listSelections().length, r = [], l = -1, i = 0; i < n; i++) { + var o = e.listSelections()[i].head; + if (!(o.line <= l)) { + var s = f(o.line + (t ? 0 : 1), 0); + e.replaceRange(` +`, s, null, "+insertLine"), e.indentLine(s.line, null, !0), r.push({ + head: s, + anchor: s + }), l = o.line + 1; + } + } + e.setSelections(r); + }), e.execCommand("indentAuto"); + } + v(x, "insertLine"), a.insertLineAfter = function (e) { + return x(e, !1); + }, a.insertLineBefore = function (e) { + return x(e, !0); + }; + function K(e, t) { + for (var n = t.ch, r = n, l = e.getLine(t.line); n && h.isWordChar(l.charAt(n - 1));) --n; + for (; r < l.length && h.isWordChar(l.charAt(r));) ++r; + return { + from: f(t.line, n), + to: f(t.line, r), + word: l.slice(n, r) + }; + } + v(K, "wordAt"), a.selectNextOccurrence = function (e) { + var t = e.getCursor("from"), + n = e.getCursor("to"), + r = e.state.sublimeFindFullWord == e.doc.sel; + if (h.cmpPos(t, n) == 0) { + var l = K(e, t); + if (!l.word) return; + e.setSelection(l.from, l.to), r = !0; + } else { + var i = e.getRange(t, n), + o = r ? new RegExp("\\b" + i + "\\b") : i, + s = e.getSearchCursor(o, n), + u = s.findNext(); + if (u || (s = e.getSearchCursor(o, f(e.firstLine(), 0)), u = s.findNext()), !u || H(e.listSelections(), s.from(), s.to())) return; + e.addSelection(s.from(), s.to()); + } + r && (e.state.sublimeFindFullWord = e.doc.sel); + }, a.skipAndSelectNextOccurrence = function (e) { + var t = e.getCursor("anchor"), + n = e.getCursor("head"); + a.selectNextOccurrence(e), h.cmpPos(t, n) != 0 && e.doc.setSelections(e.doc.listSelections().filter(function (r) { + return r.anchor != t || r.head != n; + })); + }; + function y(e, t) { + for (var n = e.listSelections(), r = [], l = 0; l < n.length; l++) { + var i = n[l], + o = e.findPosV(i.anchor, t, "line", i.anchor.goalColumn), + s = e.findPosV(i.head, t, "line", i.head.goalColumn); + o.goalColumn = i.anchor.goalColumn != null ? i.anchor.goalColumn : e.cursorCoords(i.anchor, "div").left, s.goalColumn = i.head.goalColumn != null ? i.head.goalColumn : e.cursorCoords(i.head, "div").left; + var u = { + anchor: o, + head: s + }; + r.push(i), r.push(u); + } + e.setSelections(r); + } + v(y, "addCursorToSelection"), a.addCursorToPrevLine = function (e) { + y(e, -1); + }, a.addCursorToNextLine = function (e) { + y(e, 1); + }; + function H(e, t, n) { + for (var r = 0; r < e.length; r++) if (h.cmpPos(e[r].from(), t) == 0 && h.cmpPos(e[r].to(), n) == 0) return !0; + return !1; + } + v(H, "isSelectedRange"); + var P = "(){}[]"; + function U(e) { + for (var t = e.listSelections(), n = [], r = 0; r < t.length; r++) { + var l = t[r], + i = l.head, + o = e.scanForBracket(i, -1); + if (!o) return !1; + for (;;) { + var s = e.scanForBracket(i, 1); + if (!s) return !1; + if (s.ch == P.charAt(P.indexOf(o.ch) + 1)) { + var u = f(o.pos.line, o.pos.ch + 1); + if (h.cmpPos(u, l.from()) == 0 && h.cmpPos(s.pos, l.to()) == 0) { + if (o = e.scanForBracket(o.pos, -1), !o) return !1; + } else { + n.push({ + anchor: u, + head: s.pos + }); + break; + } + } + i = f(s.pos.line, s.pos.ch + 1); + } + } + return e.setSelections(n), !0; + } + v(U, "selectBetweenBrackets"), a.selectScope = function (e) { + U(e) || e.execCommand("selectAll"); + }, a.selectBetweenBrackets = function (e) { + if (!U(e)) return h.Pass; + }; + function I(e) { + return e ? /\bpunctuation\b/.test(e) ? e : void 0 : null; + } + v(I, "puncType"), a.goToBracket = function (e) { + e.extendSelectionsBy(function (t) { + var n = e.scanForBracket(t.head, 1, I(e.getTokenTypeAt(t.head))); + if (n && h.cmpPos(n.pos, t.head) != 0) return n.pos; + var r = e.scanForBracket(t.head, -1, I(e.getTokenTypeAt(f(t.head.line, t.head.ch + 1)))); + return r && f(r.pos.line, r.pos.ch + 1) || t.head; + }); + }, a.swapLineUp = function (e) { + if (e.isReadOnly()) return h.Pass; + for (var t = e.listSelections(), n = [], r = e.firstLine() - 1, l = [], i = 0; i < t.length; i++) { + var o = t[i], + s = o.from().line - 1, + u = o.to().line; + l.push({ + anchor: f(o.anchor.line - 1, o.anchor.ch), + head: f(o.head.line - 1, o.head.ch) + }), o.to().ch == 0 && !o.empty() && --u, s > r ? n.push(s, u) : n.length && (n[n.length - 1] = u), r = u; + } + e.operation(function () { + for (var d = 0; d < n.length; d += 2) { + var p = n[d], + c = n[d + 1], + b = e.getLine(p); + e.replaceRange("", f(p, 0), f(p + 1, 0), "+swapLine"), c > e.lastLine() ? e.replaceRange(` +` + b, f(e.lastLine()), null, "+swapLine") : e.replaceRange(b + ` +`, f(c, 0), null, "+swapLine"); + } + e.setSelections(l), e.scrollIntoView(); + }); + }, a.swapLineDown = function (e) { + if (e.isReadOnly()) return h.Pass; + for (var t = e.listSelections(), n = [], r = e.lastLine() + 1, l = t.length - 1; l >= 0; l--) { + var i = t[l], + o = i.to().line + 1, + s = i.from().line; + i.to().ch == 0 && !i.empty() && o--, o < r ? n.push(o, s) : n.length && (n[n.length - 1] = s), r = s; + } + e.operation(function () { + for (var u = n.length - 2; u >= 0; u -= 2) { + var d = n[u], + p = n[u + 1], + c = e.getLine(d); + d == e.lastLine() ? e.replaceRange("", f(d - 1), f(d), "+swapLine") : e.replaceRange("", f(d, 0), f(d + 1, 0), "+swapLine"), e.replaceRange(c + ` +`, f(p, 0), null, "+swapLine"); + } + e.scrollIntoView(); + }); + }, a.toggleCommentIndented = function (e) { + e.toggleComment({ + indent: !0 + }); + }, a.joinLines = function (e) { + for (var t = e.listSelections(), n = [], r = 0; r < t.length; r++) { + for (var l = t[r], i = l.from(), o = i.line, s = l.to().line; r < t.length - 1 && t[r + 1].from().line == s;) s = t[++r].to().line; + n.push({ + start: o, + end: s, + anchor: !l.empty() && i + }); + } + e.operation(function () { + for (var u = 0, d = [], p = 0; p < n.length; p++) { + for (var c = n[p], b = c.anchor && f(c.anchor.line - u, c.anchor.ch), w, g = c.start; g <= c.end; g++) { + var S = g - u; + g == c.end && (w = f(S, e.getLine(S).length + 1)), S < e.lastLine() && (e.replaceRange(" ", f(S), f(S + 1, /^\s*/.exec(e.getLine(S + 1))[0].length)), ++u); + } + d.push({ + anchor: b || w, + head: w + }); + } + e.setSelections(d, 0); + }); + }, a.duplicateLine = function (e) { + e.operation(function () { + for (var t = e.listSelections().length, n = 0; n < t; n++) { + var r = e.listSelections()[n]; + r.empty() ? e.replaceRange(e.getLine(r.head.line) + ` +`, f(r.head.line, 0)) : e.replaceRange(e.getRange(r.from(), r.to()), r.from()); + } + e.scrollIntoView(); + }); + }; + function R(e, t, n) { + if (e.isReadOnly()) return h.Pass; + for (var r = e.listSelections(), l = [], i, o = 0; o < r.length; o++) { + var s = r[o]; + if (!s.empty()) { + for (var u = s.from().line, d = s.to().line; o < r.length - 1 && r[o + 1].from().line == d;) d = r[++o].to().line; + r[o].to().ch || d--, l.push(u, d); + } + } + l.length ? i = !0 : l.push(e.firstLine(), e.lastLine()), e.operation(function () { + for (var p = [], c = 0; c < l.length; c += 2) { + var b = l[c], + w = l[c + 1], + g = f(b, 0), + S = f(w), + F = e.getRange(g, S, !1); + t ? F.sort(function (k, L) { + return k < L ? -n : k == L ? 0 : n; + }) : F.sort(function (k, L) { + var W = k.toUpperCase(), + M = L.toUpperCase(); + return W != M && (k = W, L = M), k < L ? -n : k == L ? 0 : n; + }), e.replaceRange(F, g, S), i && p.push({ + anchor: g, + head: f(w + 1, 0) + }); + } + i && e.setSelections(p, 0); + }); + } + v(R, "sortLines"), a.sortLines = function (e) { + R(e, !0, 1); + }, a.reverseSortLines = function (e) { + R(e, !0, -1); + }, a.sortLinesInsensitive = function (e) { + R(e, !1, 1); + }, a.reverseSortLinesInsensitive = function (e) { + R(e, !1, -1); + }, a.nextBookmark = function (e) { + var t = e.state.sublimeBookmarks; + if (t) for (; t.length;) { + var n = t.shift(), + r = n.find(); + if (r) return t.push(n), e.setSelection(r.from, r.to); + } + }, a.prevBookmark = function (e) { + var t = e.state.sublimeBookmarks; + if (t) for (; t.length;) { + t.unshift(t.pop()); + var n = t[t.length - 1].find(); + if (!n) t.pop();else return e.setSelection(n.from, n.to); + } + }, a.toggleBookmark = function (e) { + for (var t = e.listSelections(), n = e.state.sublimeBookmarks || (e.state.sublimeBookmarks = []), r = 0; r < t.length; r++) { + for (var l = t[r].from(), i = t[r].to(), o = t[r].empty() ? e.findMarksAt(l) : e.findMarks(l, i), s = 0; s < o.length; s++) if (o[s].sublimeBookmark) { + o[s].clear(); + for (var u = 0; u < n.length; u++) n[u] == o[s] && n.splice(u--, 1); + break; + } + s == o.length && n.push(e.markText(l, i, { + sublimeBookmark: !0, + clearWhenEmpty: !1 + })); + } + }, a.clearBookmarks = function (e) { + var t = e.state.sublimeBookmarks; + if (t) for (var n = 0; n < t.length; n++) t[n].clear(); + t.length = 0; + }, a.selectBookmarks = function (e) { + var t = e.state.sublimeBookmarks, + n = []; + if (t) for (var r = 0; r < t.length; r++) { + var l = t[r].find(); + l ? n.push({ + anchor: l.from, + head: l.to + }) : t.splice(r--, 0); + } + n.length && e.setSelections(n, 0); + }; + function D(e, t) { + e.operation(function () { + for (var n = e.listSelections(), r = [], l = [], i = 0; i < n.length; i++) { + var o = n[i]; + o.empty() ? (r.push(i), l.push("")) : l.push(t(e.getRange(o.from(), o.to()))); + } + e.replaceSelections(l, "around", "case"); + for (var i = r.length - 1, s; i >= 0; i--) { + var o = n[r[i]]; + if (!(s && h.cmpPos(o.head, s) > 0)) { + var u = K(e, o.head); + s = u.from, e.replaceRange(t(u.word), u.from, u.to); + } + } + }); + } + v(D, "modifyWordOrSelection"), a.smartBackspace = function (e) { + if (e.somethingSelected()) return h.Pass; + e.operation(function () { + for (var t = e.listSelections(), n = e.getOption("indentUnit"), r = t.length - 1; r >= 0; r--) { + var l = t[r].head, + i = e.getRange({ + line: l.line, + ch: 0 + }, l), + o = h.countColumn(i, null, e.getOption("tabSize")), + s = e.findPosH(l, -1, "char", !1); + if (i && !/\S/.test(i) && o % n == 0) { + var u = new f(l.line, h.findColumn(i, o - n, n)); + u.ch != l.ch && (s = u); + } + e.replaceRange("", s, l, "+delete"); + } + }); + }, a.delLineRight = function (e) { + e.operation(function () { + for (var t = e.listSelections(), n = t.length - 1; n >= 0; n--) e.replaceRange("", t[n].anchor, f(t[n].to().line), "+delete"); + e.scrollIntoView(); + }); + }, a.upcaseAtCursor = function (e) { + D(e, function (t) { + return t.toUpperCase(); + }); + }, a.downcaseAtCursor = function (e) { + D(e, function (t) { + return t.toLowerCase(); + }); + }, a.setSublimeMark = function (e) { + e.state.sublimeMark && e.state.sublimeMark.clear(), e.state.sublimeMark = e.setBookmark(e.getCursor()); + }, a.selectToSublimeMark = function (e) { + var t = e.state.sublimeMark && e.state.sublimeMark.find(); + t && e.setSelection(e.getCursor(), t); + }, a.deleteToSublimeMark = function (e) { + var t = e.state.sublimeMark && e.state.sublimeMark.find(); + if (t) { + var n = e.getCursor(), + r = t; + if (h.cmpPos(n, r) > 0) { + var l = r; + r = n, n = l; + } + e.state.sublimeKilled = e.getRange(n, r), e.replaceRange("", n, r); + } + }, a.swapWithSublimeMark = function (e) { + var t = e.state.sublimeMark && e.state.sublimeMark.find(); + t && (e.state.sublimeMark.clear(), e.state.sublimeMark = e.setBookmark(e.getCursor()), e.setCursor(t)); + }, a.sublimeYank = function (e) { + e.state.sublimeKilled != null && e.replaceSelection(e.state.sublimeKilled, null, "paste"); + }, a.showInCenter = function (e) { + var t = e.cursorCoords(null, "local"); + e.scrollTo(null, (t.top + t.bottom) / 2 - e.getScrollInfo().clientHeight / 2); + }; + function N(e) { + var t = e.getCursor("from"), + n = e.getCursor("to"); + if (h.cmpPos(t, n) == 0) { + var r = K(e, t); + if (!r.word) return; + t = r.from, n = r.to; + } + return { + from: t, + to: n, + query: e.getRange(t, n), + word: r + }; + } + v(N, "getTarget"); + function O(e, t) { + var n = N(e); + if (n) { + var r = n.query, + l = e.getSearchCursor(r, t ? n.to : n.from); + (t ? l.findNext() : l.findPrevious()) ? e.setSelection(l.from(), l.to()) : (l = e.getSearchCursor(r, t ? f(e.firstLine(), 0) : e.clipPos(f(e.lastLine()))), (t ? l.findNext() : l.findPrevious()) ? e.setSelection(l.from(), l.to()) : n.word && e.setSelection(n.from, n.to)); + } + } + v(O, "findAndGoTo"), a.findUnder = function (e) { + O(e, !0); + }, a.findUnderPrevious = function (e) { + O(e, !1); + }, a.findAllUnder = function (e) { + var t = N(e); + if (t) { + for (var n = e.getSearchCursor(t.query), r = [], l = -1; n.findNext();) r.push({ + anchor: n.from(), + head: n.to() + }), n.from().line <= t.from.line && n.from().ch <= t.from.ch && l++; + e.setSelections(r, l); + } + }; + var C = h.keyMap; + C.macSublime = { + "Cmd-Left": "goLineStartSmart", + "Shift-Tab": "indentLess", + "Shift-Ctrl-K": "deleteLine", + "Alt-Q": "wrapLines", + "Ctrl-Left": "goSubwordLeft", + "Ctrl-Right": "goSubwordRight", + "Ctrl-Alt-Up": "scrollLineUp", + "Ctrl-Alt-Down": "scrollLineDown", + "Cmd-L": "selectLine", + "Shift-Cmd-L": "splitSelectionByLine", + Esc: "singleSelectionTop", + "Cmd-Enter": "insertLineAfter", + "Shift-Cmd-Enter": "insertLineBefore", + "Cmd-D": "selectNextOccurrence", + "Shift-Cmd-Space": "selectScope", + "Shift-Cmd-M": "selectBetweenBrackets", + "Cmd-M": "goToBracket", + "Cmd-Ctrl-Up": "swapLineUp", + "Cmd-Ctrl-Down": "swapLineDown", + "Cmd-/": "toggleCommentIndented", + "Cmd-J": "joinLines", + "Shift-Cmd-D": "duplicateLine", + F5: "sortLines", + "Shift-F5": "reverseSortLines", + "Cmd-F5": "sortLinesInsensitive", + "Shift-Cmd-F5": "reverseSortLinesInsensitive", + F2: "nextBookmark", + "Shift-F2": "prevBookmark", + "Cmd-F2": "toggleBookmark", + "Shift-Cmd-F2": "clearBookmarks", + "Alt-F2": "selectBookmarks", + Backspace: "smartBackspace", + "Cmd-K Cmd-D": "skipAndSelectNextOccurrence", + "Cmd-K Cmd-K": "delLineRight", + "Cmd-K Cmd-U": "upcaseAtCursor", + "Cmd-K Cmd-L": "downcaseAtCursor", + "Cmd-K Cmd-Space": "setSublimeMark", + "Cmd-K Cmd-A": "selectToSublimeMark", + "Cmd-K Cmd-W": "deleteToSublimeMark", + "Cmd-K Cmd-X": "swapWithSublimeMark", + "Cmd-K Cmd-Y": "sublimeYank", + "Cmd-K Cmd-C": "showInCenter", + "Cmd-K Cmd-G": "clearBookmarks", + "Cmd-K Cmd-Backspace": "delLineLeft", + "Cmd-K Cmd-1": "foldAll", + "Cmd-K Cmd-0": "unfoldAll", + "Cmd-K Cmd-J": "unfoldAll", + "Ctrl-Shift-Up": "addCursorToPrevLine", + "Ctrl-Shift-Down": "addCursorToNextLine", + "Cmd-F3": "findUnder", + "Shift-Cmd-F3": "findUnderPrevious", + "Alt-F3": "findAllUnder", + "Shift-Cmd-[": "fold", + "Shift-Cmd-]": "unfold", + "Cmd-I": "findIncremental", + "Shift-Cmd-I": "findIncrementalReverse", + "Cmd-H": "replace", + F3: "findNext", + "Shift-F3": "findPrev", + fallthrough: "macDefault" + }, h.normalizeKeyMap(C.macSublime), C.pcSublime = { + "Shift-Tab": "indentLess", + "Shift-Ctrl-K": "deleteLine", + "Alt-Q": "wrapLines", + "Ctrl-T": "transposeChars", + "Alt-Left": "goSubwordLeft", + "Alt-Right": "goSubwordRight", + "Ctrl-Up": "scrollLineUp", + "Ctrl-Down": "scrollLineDown", + "Ctrl-L": "selectLine", + "Shift-Ctrl-L": "splitSelectionByLine", + Esc: "singleSelectionTop", + "Ctrl-Enter": "insertLineAfter", + "Shift-Ctrl-Enter": "insertLineBefore", + "Ctrl-D": "selectNextOccurrence", + "Shift-Ctrl-Space": "selectScope", + "Shift-Ctrl-M": "selectBetweenBrackets", + "Ctrl-M": "goToBracket", + "Shift-Ctrl-Up": "swapLineUp", + "Shift-Ctrl-Down": "swapLineDown", + "Ctrl-/": "toggleCommentIndented", + "Ctrl-J": "joinLines", + "Shift-Ctrl-D": "duplicateLine", + F9: "sortLines", + "Shift-F9": "reverseSortLines", + "Ctrl-F9": "sortLinesInsensitive", + "Shift-Ctrl-F9": "reverseSortLinesInsensitive", + F2: "nextBookmark", + "Shift-F2": "prevBookmark", + "Ctrl-F2": "toggleBookmark", + "Shift-Ctrl-F2": "clearBookmarks", + "Alt-F2": "selectBookmarks", + Backspace: "smartBackspace", + "Ctrl-K Ctrl-D": "skipAndSelectNextOccurrence", + "Ctrl-K Ctrl-K": "delLineRight", + "Ctrl-K Ctrl-U": "upcaseAtCursor", + "Ctrl-K Ctrl-L": "downcaseAtCursor", + "Ctrl-K Ctrl-Space": "setSublimeMark", + "Ctrl-K Ctrl-A": "selectToSublimeMark", + "Ctrl-K Ctrl-W": "deleteToSublimeMark", + "Ctrl-K Ctrl-X": "swapWithSublimeMark", + "Ctrl-K Ctrl-Y": "sublimeYank", + "Ctrl-K Ctrl-C": "showInCenter", + "Ctrl-K Ctrl-G": "clearBookmarks", + "Ctrl-K Ctrl-Backspace": "delLineLeft", + "Ctrl-K Ctrl-1": "foldAll", + "Ctrl-K Ctrl-0": "unfoldAll", + "Ctrl-K Ctrl-J": "unfoldAll", + "Ctrl-Alt-Up": "addCursorToPrevLine", + "Ctrl-Alt-Down": "addCursorToNextLine", + "Ctrl-F3": "findUnder", + "Shift-Ctrl-F3": "findUnderPrevious", + "Alt-F3": "findAllUnder", + "Shift-Ctrl-[": "fold", + "Shift-Ctrl-]": "unfold", + "Ctrl-I": "findIncremental", + "Shift-Ctrl-I": "findIncrementalReverse", + "Ctrl-H": "replace", + F3: "findNext", + "Shift-F3": "findPrev", + fallthrough: "pcDefault" + }, h.normalizeKeyMap(C.pcSublime); + var V = C.default == C.macDefault; + C.sublime = V ? C.macSublime : C.pcSublime; + }); +})(); +var q = G.exports; +const Q = E.getDefaultExportFromCjs(q), + X = J({ + __proto__: null, + default: Q + }, [q]); +exports.sublime = X; + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/async-helpers/index.js": +/*!*********************************************************!*\ + !*** ../../graphiql-toolkit/esm/async-helpers/index.js ***! + \*********************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.fetcherReturnToPromise = fetcherReturnToPromise; +exports.isAsyncIterable = isAsyncIterable; +exports.isObservable = isObservable; +exports.isPromise = isPromise; +var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P ? value : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +function isPromise(value) { + return typeof value === 'object' && value !== null && typeof value.then === 'function'; +} +function observableToPromise(observable) { + return new Promise((resolve, reject) => { + const subscription = observable.subscribe({ + next(v) { + resolve(v); + subscription.unsubscribe(); + }, + error: reject, + complete() { + reject(new Error('no value resolved')); + } + }); + }); +} +function isObservable(value) { + return typeof value === 'object' && value !== null && 'subscribe' in value && typeof value.subscribe === 'function'; +} +function isAsyncIterable(input) { + return typeof input === 'object' && input !== null && (input[Symbol.toStringTag] === 'AsyncGenerator' || Symbol.asyncIterator in input); +} +function asyncIterableToPromise(input) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const iteratorReturn = (_a = ('return' in input ? input : input[Symbol.asyncIterator]()).return) === null || _a === void 0 ? void 0 : _a.bind(input); + const iteratorNext = ('next' in input ? input : input[Symbol.asyncIterator]()).next.bind(input); + const result = yield iteratorNext(); + void (iteratorReturn === null || iteratorReturn === void 0 ? void 0 : iteratorReturn()); + return result.value; + }); +} +function fetcherReturnToPromise(fetcherResult) { + return __awaiter(this, void 0, void 0, function* () { + const result = yield fetcherResult; + if (isAsyncIterable(result)) { + return asyncIterableToPromise(result); + } + if (isObservable(result)) { + return observableToPromise(result); + } + return result; + }); +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/create-fetcher/createFetcher.js": +/*!******************************************************************!*\ + !*** ../../graphiql-toolkit/esm/create-fetcher/createFetcher.js ***! + \******************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createGraphiQLFetcher = createGraphiQLFetcher; +var _lib = __webpack_require__(/*! ./lib */ "../../graphiql-toolkit/esm/create-fetcher/lib.js"); +function createGraphiQLFetcher(options) { + let httpFetch; + if (typeof window !== 'undefined' && window.fetch) { + httpFetch = window.fetch; + } + if ((options === null || options === void 0 ? void 0 : options.enableIncrementalDelivery) === null || options.enableIncrementalDelivery !== false) { + options.enableIncrementalDelivery = true; + } + if (options.fetch) { + httpFetch = options.fetch; + } + if (!httpFetch) { + throw new Error('No valid fetcher implementation available'); + } + const simpleFetcher = (0, _lib.createSimpleFetcher)(options, httpFetch); + const httpFetcher = options.enableIncrementalDelivery ? (0, _lib.createMultipartFetcher)(options, httpFetch) : simpleFetcher; + return (graphQLParams, fetcherOpts) => { + if (graphQLParams.operationName === 'IntrospectionQuery') { + return (options.schemaFetcher || simpleFetcher)(graphQLParams, fetcherOpts); + } + const isSubscription = (fetcherOpts === null || fetcherOpts === void 0 ? void 0 : fetcherOpts.documentAST) ? (0, _lib.isSubscriptionWithName)(fetcherOpts.documentAST, graphQLParams.operationName || undefined) : false; + if (isSubscription) { + const wsFetcher = (0, _lib.getWsFetcher)(options, fetcherOpts); + if (!wsFetcher) { + throw new Error(`Your GraphiQL createFetcher is not properly configured for websocket subscriptions yet. ${options.subscriptionUrl ? `Provided URL ${options.subscriptionUrl} failed` : 'Please provide subscriptionUrl, wsClient or legacyClient option first.'}`); + } + return wsFetcher(graphQLParams); + } + return httpFetcher(graphQLParams, fetcherOpts); + }; +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/create-fetcher/index.js": +/*!**********************************************************!*\ + !*** ../../graphiql-toolkit/esm/create-fetcher/index.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _exportNames = { + createGraphiQLFetcher: true +}; +Object.defineProperty(exports, "createGraphiQLFetcher", ({ + enumerable: true, + get: function () { + return _createFetcher.createGraphiQLFetcher; + } +})); +var _types = __webpack_require__(/*! ./types */ "../../graphiql-toolkit/esm/create-fetcher/types.js"); +Object.keys(_types).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _types[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _types[key]; + } + }); +}); +var _createFetcher = __webpack_require__(/*! ./createFetcher */ "../../graphiql-toolkit/esm/create-fetcher/createFetcher.js"); + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/create-fetcher/lib.js": +/*!********************************************************!*\ + !*** ../../graphiql-toolkit/esm/create-fetcher/lib.js ***! + \********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isSubscriptionWithName = exports.getWsFetcher = exports.createWebsocketsFetcherFromUrl = exports.createWebsocketsFetcherFromClient = exports.createSimpleFetcher = exports.createMultipartFetcher = exports.createLegacyWebsocketsFetcher = void 0; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _meros = __webpack_require__(/*! meros */ "../../../node_modules/meros/browser/index.mjs"); +var _pushPullAsyncIterableIterator = __webpack_require__(/*! @n1ru4l/push-pull-async-iterable-iterator */ "../../../node_modules/@n1ru4l/push-pull-async-iterable-iterator/index.js"); +var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P ? value : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __await = void 0 && (void 0).__await || function (v) { + return this instanceof __await ? (this.v = v, this) : new __await(v); +}; +var __asyncValues = void 0 && (void 0).__asyncValues || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], + i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { + return this; + }, i); + function verb(n) { + i[n] = o[n] && function (v) { + return new Promise(function (resolve, reject) { + v = o[n](v), settle(resolve, reject, v.done, v.value); + }); + }; + } + function settle(resolve, reject, d, v) { + Promise.resolve(v).then(function (v) { + resolve({ + value: v, + done: d + }); + }, reject); + } +}; +var __asyncGenerator = void 0 && (void 0).__asyncGenerator || function (thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), + i, + q = []; + return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { + return this; + }, i; + function verb(n) { + if (g[n]) i[n] = function (v) { + return new Promise(function (a, b) { + q.push([n, v, a, b]) > 1 || resume(n, v); + }); + }; + } + function resume(n, v) { + try { + step(g[n](v)); + } catch (e) { + settle(q[0][3], e); + } + } + function step(r) { + r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); + } + function fulfill(value) { + resume("next", value); + } + function reject(value) { + resume("throw", value); + } + function settle(f, v) { + if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); + } +}; +const errorHasCode = err => { + return typeof err === 'object' && err !== null && 'code' in err; +}; +const isSubscriptionWithName = (document, name) => { + let isSubscription = false; + (0, _graphql.visit)(document, { + OperationDefinition(node) { + var _a; + if (name === ((_a = node.name) === null || _a === void 0 ? void 0 : _a.value) && node.operation === 'subscription') { + isSubscription = true; + } + } + }); + return isSubscription; +}; +exports.isSubscriptionWithName = isSubscriptionWithName; +const createSimpleFetcher = (options, httpFetch) => (graphQLParams, fetcherOpts) => __awaiter(void 0, void 0, void 0, function* () { + const data = yield httpFetch(options.url, { + method: 'POST', + body: JSON.stringify(graphQLParams), + headers: Object.assign(Object.assign({ + 'content-type': 'application/json' + }, options.headers), fetcherOpts === null || fetcherOpts === void 0 ? void 0 : fetcherOpts.headers) + }); + return data.json(); +}); +exports.createSimpleFetcher = createSimpleFetcher; +const createWebsocketsFetcherFromUrl = (url, connectionParams) => { + let wsClient; + try { + const { + createClient + } = __webpack_require__(/*! graphql-ws */ "../../../node_modules/graphql-ws/lib/index.js"); + wsClient = createClient({ + url, + connectionParams + }); + return createWebsocketsFetcherFromClient(wsClient); + } catch (err) { + if (errorHasCode(err) && err.code === 'MODULE_NOT_FOUND') { + throw new Error("You need to install the 'graphql-ws' package to use websockets when passing a 'subscriptionUrl'"); + } + console.error(`Error creating websocket client for ${url}`, err); + } +}; +exports.createWebsocketsFetcherFromUrl = createWebsocketsFetcherFromUrl; +const createWebsocketsFetcherFromClient = wsClient => graphQLParams => (0, _pushPullAsyncIterableIterator.makeAsyncIterableIteratorFromSink)(sink => wsClient.subscribe(graphQLParams, Object.assign(Object.assign({}, sink), { + error(err) { + if (err instanceof CloseEvent) { + sink.error(new Error(`Socket closed with event ${err.code} ${err.reason || ''}`.trim())); + } else { + sink.error(err); + } + } +}))); +exports.createWebsocketsFetcherFromClient = createWebsocketsFetcherFromClient; +const createLegacyWebsocketsFetcher = legacyWsClient => graphQLParams => { + const observable = legacyWsClient.request(graphQLParams); + return (0, _pushPullAsyncIterableIterator.makeAsyncIterableIteratorFromSink)(sink => observable.subscribe(sink).unsubscribe); +}; +exports.createLegacyWebsocketsFetcher = createLegacyWebsocketsFetcher; +const createMultipartFetcher = (options, httpFetch) => function (graphQLParams, fetcherOpts) { + return __asyncGenerator(this, arguments, function* () { + var e_1, _a; + const response = yield __await(httpFetch(options.url, { + method: 'POST', + body: JSON.stringify(graphQLParams), + headers: Object.assign(Object.assign({ + 'content-type': 'application/json', + accept: 'application/json, multipart/mixed' + }, options.headers), fetcherOpts === null || fetcherOpts === void 0 ? void 0 : fetcherOpts.headers) + }).then(r => (0, _meros.meros)(r, { + multiple: true + }))); + if (!(0, _pushPullAsyncIterableIterator.isAsyncIterable)(response)) { + return yield __await(yield yield __await(response.json())); + } + try { + for (var response_1 = __asyncValues(response), response_1_1; response_1_1 = yield __await(response_1.next()), !response_1_1.done;) { + const chunk = response_1_1.value; + if (chunk.some(part => !part.json)) { + const message = chunk.map(part => `Headers::\n${part.headers}\n\nBody::\n${part.body}`); + throw new Error(`Expected multipart chunks to be of json type. got:\n${message}`); + } + yield yield __await(chunk.map(part => part.body)); + } + } catch (e_1_1) { + e_1 = { + error: e_1_1 + }; + } finally { + try { + if (response_1_1 && !response_1_1.done && (_a = response_1.return)) yield __await(_a.call(response_1)); + } finally { + if (e_1) throw e_1.error; + } + } + }); +}; +exports.createMultipartFetcher = createMultipartFetcher; +const getWsFetcher = (options, fetcherOpts) => { + if (options.wsClient) { + return createWebsocketsFetcherFromClient(options.wsClient); + } + if (options.subscriptionUrl) { + return createWebsocketsFetcherFromUrl(options.subscriptionUrl, Object.assign(Object.assign({}, options.wsConnectionParams), fetcherOpts === null || fetcherOpts === void 0 ? void 0 : fetcherOpts.headers)); + } + const legacyWebsocketsClient = options.legacyClient || options.legacyWsClient; + if (legacyWebsocketsClient) { + return createLegacyWebsocketsFetcher(legacyWebsocketsClient); + } +}; +exports.getWsFetcher = getWsFetcher; + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/create-fetcher/types.js": +/*!**********************************************************!*\ + !*** ../../graphiql-toolkit/esm/create-fetcher/types.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/format/index.js": +/*!**************************************************!*\ + !*** ../../graphiql-toolkit/esm/format/index.js ***! + \**************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.formatError = formatError; +exports.formatResult = formatResult; +function stringify(obj) { + return JSON.stringify(obj, null, 2); +} +function formatSingleError(error) { + return Object.assign(Object.assign({}, error), { + message: error.message, + stack: error.stack + }); +} +function handleSingleError(error) { + if (error instanceof Error) { + return formatSingleError(error); + } + return error; +} +function formatError(error) { + if (Array.isArray(error)) { + return stringify({ + errors: error.map(e => handleSingleError(e)) + }); + } + return stringify({ + errors: [handleSingleError(error)] + }); +} +function formatResult(result) { + return stringify(result); +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/graphql-helpers/auto-complete.js": +/*!*******************************************************************!*\ + !*** ../../graphiql-toolkit/esm/graphql-helpers/auto-complete.js ***! + \*******************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.fillLeafs = fillLeafs; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function fillLeafs(schema, docString, getDefaultFieldNames) { + const insertions = []; + if (!schema || !docString) { + return { + insertions, + result: docString + }; + } + let ast; + try { + ast = (0, _graphql.parse)(docString); + } catch (_a) { + return { + insertions, + result: docString + }; + } + const fieldNameFn = getDefaultFieldNames || defaultGetDefaultFieldNames; + const typeInfo = new _graphql.TypeInfo(schema); + (0, _graphql.visit)(ast, { + leave(node) { + typeInfo.leave(node); + }, + enter(node) { + typeInfo.enter(node); + if (node.kind === 'Field' && !node.selectionSet) { + const fieldType = typeInfo.getType(); + const selectionSet = buildSelectionSet(isFieldType(fieldType), fieldNameFn); + if (selectionSet && node.loc) { + const indent = getIndentation(docString, node.loc.start); + insertions.push({ + index: node.loc.end, + string: ' ' + (0, _graphql.print)(selectionSet).replaceAll('\n', '\n' + indent) + }); + } + } + } + }); + return { + insertions, + result: withInsertions(docString, insertions) + }; +} +function defaultGetDefaultFieldNames(type) { + if (!('getFields' in type)) { + return []; + } + const fields = type.getFields(); + if (fields.id) { + return ['id']; + } + if (fields.edges) { + return ['edges']; + } + if (fields.node) { + return ['node']; + } + const leafFieldNames = []; + for (const fieldName of Object.keys(fields)) { + if ((0, _graphql.isLeafType)(fields[fieldName].type)) { + leafFieldNames.push(fieldName); + } + } + return leafFieldNames; +} +function buildSelectionSet(type, getDefaultFieldNames) { + const namedType = (0, _graphql.getNamedType)(type); + if (!type || (0, _graphql.isLeafType)(type)) { + return; + } + const fieldNames = getDefaultFieldNames(namedType); + if (!Array.isArray(fieldNames) || fieldNames.length === 0 || !('getFields' in namedType)) { + return; + } + return { + kind: _graphql.Kind.SELECTION_SET, + selections: fieldNames.map(fieldName => { + const fieldDef = namedType.getFields()[fieldName]; + const fieldType = fieldDef ? fieldDef.type : null; + return { + kind: _graphql.Kind.FIELD, + name: { + kind: _graphql.Kind.NAME, + value: fieldName + }, + selectionSet: buildSelectionSet(fieldType, getDefaultFieldNames) + }; + }) + }; +} +function withInsertions(initial, insertions) { + if (insertions.length === 0) { + return initial; + } + let edited = ''; + let prevIndex = 0; + for (const { + index, + string + } of insertions) { + edited += initial.slice(prevIndex, index) + string; + prevIndex = index; + } + edited += initial.slice(prevIndex); + return edited; +} +function getIndentation(str, index) { + let indentStart = index; + let indentEnd = index; + while (indentStart) { + const c = str.charCodeAt(indentStart - 1); + if (c === 10 || c === 13 || c === 0x2028 || c === 0x2029) { + break; + } + indentStart--; + if (c !== 9 && c !== 11 && c !== 12 && c !== 32 && c !== 160) { + indentEnd = indentStart; + } + } + return str.slice(indentStart, indentEnd); +} +function isFieldType(fieldType) { + if (fieldType) { + return fieldType; + } +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/graphql-helpers/index.js": +/*!***********************************************************!*\ + !*** ../../graphiql-toolkit/esm/graphql-helpers/index.js ***! + \***********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _autoComplete = __webpack_require__(/*! ./auto-complete */ "../../graphiql-toolkit/esm/graphql-helpers/auto-complete.js"); +Object.keys(_autoComplete).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _autoComplete[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _autoComplete[key]; + } + }); +}); +var _mergeAst = __webpack_require__(/*! ./merge-ast */ "../../graphiql-toolkit/esm/graphql-helpers/merge-ast.js"); +Object.keys(_mergeAst).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _mergeAst[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _mergeAst[key]; + } + }); +}); +var _operationName = __webpack_require__(/*! ./operation-name */ "../../graphiql-toolkit/esm/graphql-helpers/operation-name.js"); +Object.keys(_operationName).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _operationName[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _operationName[key]; + } + }); +}); + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/graphql-helpers/merge-ast.js": +/*!***************************************************************!*\ + !*** ../../graphiql-toolkit/esm/graphql-helpers/merge-ast.js ***! + \***************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.mergeAst = mergeAst; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function uniqueBy(array, iteratee) { + var _a; + const FilteredMap = new Map(); + const result = []; + for (const item of array) { + if (item.kind === 'Field') { + const uniqueValue = iteratee(item); + const existing = FilteredMap.get(uniqueValue); + if ((_a = item.directives) === null || _a === void 0 ? void 0 : _a.length) { + const itemClone = Object.assign({}, item); + result.push(itemClone); + } else if ((existing === null || existing === void 0 ? void 0 : existing.selectionSet) && item.selectionSet) { + existing.selectionSet.selections = [...existing.selectionSet.selections, ...item.selectionSet.selections]; + } else if (!existing) { + const itemClone = Object.assign({}, item); + FilteredMap.set(uniqueValue, itemClone); + result.push(itemClone); + } + } else { + result.push(item); + } + } + return result; +} +function inlineRelevantFragmentSpreads(fragmentDefinitions, selections, selectionSetType) { + var _a; + const selectionSetTypeName = selectionSetType ? (0, _graphql.getNamedType)(selectionSetType).name : null; + const outputSelections = []; + const seenSpreads = []; + for (let selection of selections) { + if (selection.kind === 'FragmentSpread') { + const fragmentName = selection.name.value; + if (!selection.directives || selection.directives.length === 0) { + if (seenSpreads.includes(fragmentName)) { + continue; + } else { + seenSpreads.push(fragmentName); + } + } + const fragmentDefinition = fragmentDefinitions[selection.name.value]; + if (fragmentDefinition) { + const { + typeCondition, + directives, + selectionSet + } = fragmentDefinition; + selection = { + kind: _graphql.Kind.INLINE_FRAGMENT, + typeCondition, + directives, + selectionSet + }; + } + } + if (selection.kind === _graphql.Kind.INLINE_FRAGMENT && (!selection.directives || ((_a = selection.directives) === null || _a === void 0 ? void 0 : _a.length) === 0)) { + const fragmentTypeName = selection.typeCondition ? selection.typeCondition.name.value : null; + if (!fragmentTypeName || fragmentTypeName === selectionSetTypeName) { + outputSelections.push(...inlineRelevantFragmentSpreads(fragmentDefinitions, selection.selectionSet.selections, selectionSetType)); + continue; + } + } + outputSelections.push(selection); + } + return outputSelections; +} +function mergeAst(documentAST, schema) { + const typeInfo = schema ? new _graphql.TypeInfo(schema) : null; + const fragmentDefinitions = Object.create(null); + for (const definition of documentAST.definitions) { + if (definition.kind === _graphql.Kind.FRAGMENT_DEFINITION) { + fragmentDefinitions[definition.name.value] = definition; + } + } + const flattenVisitors = { + SelectionSet(node) { + const selectionSetType = typeInfo ? typeInfo.getParentType() : null; + let { + selections + } = node; + selections = inlineRelevantFragmentSpreads(fragmentDefinitions, selections, selectionSetType); + return Object.assign(Object.assign({}, node), { + selections + }); + }, + FragmentDefinition() { + return null; + } + }; + const flattenedAST = (0, _graphql.visit)(documentAST, typeInfo ? (0, _graphql.visitWithTypeInfo)(typeInfo, flattenVisitors) : flattenVisitors); + const deduplicateVisitors = { + SelectionSet(node) { + let { + selections + } = node; + selections = uniqueBy(selections, selection => selection.alias ? selection.alias.value : selection.name.value); + return Object.assign(Object.assign({}, node), { + selections + }); + }, + FragmentDefinition() { + return null; + } + }; + return (0, _graphql.visit)(flattenedAST, deduplicateVisitors); +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/graphql-helpers/operation-name.js": +/*!********************************************************************!*\ + !*** ../../graphiql-toolkit/esm/graphql-helpers/operation-name.js ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getSelectedOperationName = getSelectedOperationName; +function getSelectedOperationName(prevOperations, prevSelectedOperationName, operations) { + if (!operations || operations.length < 1) { + return; + } + const names = operations.map(op => { + var _a; + return (_a = op.name) === null || _a === void 0 ? void 0 : _a.value; + }); + if (prevSelectedOperationName && names.includes(prevSelectedOperationName)) { + return prevSelectedOperationName; + } + if (prevSelectedOperationName && prevOperations) { + const prevNames = prevOperations.map(op => { + var _a; + return (_a = op.name) === null || _a === void 0 ? void 0 : _a.value; + }); + const prevIndex = prevNames.indexOf(prevSelectedOperationName); + if (prevIndex !== -1 && prevIndex < names.length) { + return names[prevIndex]; + } + } + return names[0]; +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/index.js": +/*!*******************************************!*\ + !*** ../../graphiql-toolkit/esm/index.js ***! + \*******************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _asyncHelpers = __webpack_require__(/*! ./async-helpers */ "../../graphiql-toolkit/esm/async-helpers/index.js"); +Object.keys(_asyncHelpers).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _asyncHelpers[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _asyncHelpers[key]; + } + }); +}); +var _createFetcher = __webpack_require__(/*! ./create-fetcher */ "../../graphiql-toolkit/esm/create-fetcher/index.js"); +Object.keys(_createFetcher).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _createFetcher[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _createFetcher[key]; + } + }); +}); +var _format = __webpack_require__(/*! ./format */ "../../graphiql-toolkit/esm/format/index.js"); +Object.keys(_format).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _format[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _format[key]; + } + }); +}); +var _graphqlHelpers = __webpack_require__(/*! ./graphql-helpers */ "../../graphiql-toolkit/esm/graphql-helpers/index.js"); +Object.keys(_graphqlHelpers).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _graphqlHelpers[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _graphqlHelpers[key]; + } + }); +}); +var _storage = __webpack_require__(/*! ./storage */ "../../graphiql-toolkit/esm/storage/index.js"); +Object.keys(_storage).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _storage[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _storage[key]; + } + }); +}); + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/storage/base.js": +/*!**************************************************!*\ + !*** ../../graphiql-toolkit/esm/storage/base.js ***! + \**************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.StorageAPI = void 0; +function isQuotaError(storage, e) { + return e instanceof DOMException && (e.code === 22 || e.code === 1014 || e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && storage.length !== 0; +} +class StorageAPI { + constructor(storage) { + if (storage) { + this.storage = storage; + } else if (storage === null) { + this.storage = null; + } else if (typeof window === 'undefined') { + this.storage = null; + } else { + this.storage = { + getItem: window.localStorage.getItem.bind(window.localStorage), + setItem: window.localStorage.setItem.bind(window.localStorage), + removeItem: window.localStorage.removeItem.bind(window.localStorage), + get length() { + let keys = 0; + for (const key in window.localStorage) { + if (key.indexOf(`${STORAGE_NAMESPACE}:`) === 0) { + keys += 1; + } + } + return keys; + }, + clear() { + for (const key in window.localStorage) { + if (key.indexOf(`${STORAGE_NAMESPACE}:`) === 0) { + window.localStorage.removeItem(key); + } + } + } + }; + } + } + get(name) { + if (!this.storage) { + return null; + } + const key = `${STORAGE_NAMESPACE}:${name}`; + const value = this.storage.getItem(key); + if (value === 'null' || value === 'undefined') { + this.storage.removeItem(key); + return null; + } + return value || null; + } + set(name, value) { + let quotaError = false; + let error = null; + if (this.storage) { + const key = `${STORAGE_NAMESPACE}:${name}`; + if (value) { + try { + this.storage.setItem(key, value); + } catch (e) { + error = e instanceof Error ? e : new Error(`${e}`); + quotaError = isQuotaError(this.storage, e); + } + } else { + this.storage.removeItem(key); + } + } + return { + isQuotaError: quotaError, + error + }; + } + clear() { + if (this.storage) { + this.storage.clear(); + } + } +} +exports.StorageAPI = StorageAPI; +const STORAGE_NAMESPACE = 'graphiql'; + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/storage/custom.js": +/*!****************************************************!*\ + !*** ../../graphiql-toolkit/esm/storage/custom.js ***! + \****************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createLocalStorage = createLocalStorage; +function createLocalStorage(_ref) { + let { + namespace + } = _ref; + const storageKeyPrefix = `${namespace}:`; + const getStorageKey = key => `${storageKeyPrefix}${key}`; + const storage = { + setItem: (key, value) => localStorage.setItem(getStorageKey(key), value), + getItem: key => localStorage.getItem(getStorageKey(key)), + removeItem: key => localStorage.removeItem(getStorageKey(key)), + get length() { + let keys = 0; + for (const key in window.localStorage) { + if (key.indexOf(storageKeyPrefix) === 0) { + keys += 1; + } + } + return keys; + }, + clear() { + for (const key in window.localStorage) { + if (key.indexOf(storageKeyPrefix) === 0) { + window.localStorage.removeItem(key); + } + } + } + }; + return storage; +} + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/storage/history.js": +/*!*****************************************************!*\ + !*** ../../graphiql-toolkit/esm/storage/history.js ***! + \*****************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.HistoryStore = void 0; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _query = __webpack_require__(/*! ./query */ "../../graphiql-toolkit/esm/storage/query.js"); +const MAX_QUERY_SIZE = 100000; +class HistoryStore { + constructor(storage, maxHistoryLength) { + var _this = this; + this.storage = storage; + this.maxHistoryLength = maxHistoryLength; + this.updateHistory = _ref => { + let { + query, + variables, + headers, + operationName + } = _ref; + if (!this.shouldSaveQuery(query, variables, headers, this.history.fetchRecent())) { + return; + } + this.history.push({ + query, + variables, + headers, + operationName + }); + const historyQueries = this.history.items; + const favoriteQueries = this.favorite.items; + this.queries = historyQueries.concat(favoriteQueries); + }; + this.deleteHistory = function (_ref2) { + let { + query, + variables, + headers, + operationName, + favorite + } = _ref2; + let clearFavorites = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + function deleteFromStore(store) { + const found = store.items.find(x => x.query === query && x.variables === variables && x.headers === headers && x.operationName === operationName); + if (found) { + store.delete(found); + } + } + if (favorite || clearFavorites) { + deleteFromStore(_this.favorite); + } + if (!favorite || clearFavorites) { + deleteFromStore(_this.history); + } + _this.queries = [..._this.history.items, ..._this.favorite.items]; + }; + this.history = new _query.QueryStore('queries', this.storage, this.maxHistoryLength); + this.favorite = new _query.QueryStore('favorites', this.storage, null); + this.queries = [...this.history.fetchAll(), ...this.favorite.fetchAll()]; + } + shouldSaveQuery(query, variables, headers, lastQuerySaved) { + if (!query) { + return false; + } + try { + (0, _graphql.parse)(query); + } catch (_a) { + return false; + } + if (query.length > MAX_QUERY_SIZE) { + return false; + } + if (!lastQuerySaved) { + return true; + } + if (JSON.stringify(query) === JSON.stringify(lastQuerySaved.query)) { + if (JSON.stringify(variables) === JSON.stringify(lastQuerySaved.variables)) { + if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) { + return false; + } + if (headers && !lastQuerySaved.headers) { + return false; + } + } + if (variables && !lastQuerySaved.variables) { + return false; + } + } + return true; + } + toggleFavorite(_ref3) { + let { + query, + variables, + headers, + operationName, + label, + favorite + } = _ref3; + const item = { + query, + variables, + headers, + operationName, + label + }; + if (favorite) { + item.favorite = false; + this.favorite.delete(item); + this.history.push(item); + } else { + item.favorite = true; + this.favorite.push(item); + this.history.delete(item); + } + this.queries = [...this.history.items, ...this.favorite.items]; + } + editLabel(_ref4, index) { + let { + query, + variables, + headers, + operationName, + label, + favorite + } = _ref4; + const item = { + query, + variables, + headers, + operationName, + label + }; + if (favorite) { + this.favorite.edit(Object.assign(Object.assign({}, item), { + favorite + }), index); + } else { + this.history.edit(item, index); + } + this.queries = [...this.history.items, ...this.favorite.items]; + } +} +exports.HistoryStore = HistoryStore; + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/storage/index.js": +/*!***************************************************!*\ + !*** ../../graphiql-toolkit/esm/storage/index.js ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _base = __webpack_require__(/*! ./base */ "../../graphiql-toolkit/esm/storage/base.js"); +Object.keys(_base).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _base[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _base[key]; + } + }); +}); +var _history = __webpack_require__(/*! ./history */ "../../graphiql-toolkit/esm/storage/history.js"); +Object.keys(_history).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _history[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _history[key]; + } + }); +}); +var _query = __webpack_require__(/*! ./query */ "../../graphiql-toolkit/esm/storage/query.js"); +Object.keys(_query).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _query[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _query[key]; + } + }); +}); +var _custom = __webpack_require__(/*! ./custom */ "../../graphiql-toolkit/esm/storage/custom.js"); +Object.keys(_custom).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _custom[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _custom[key]; + } + }); +}); + +/***/ }), + +/***/ "../../graphiql-toolkit/esm/storage/query.js": +/*!***************************************************!*\ + !*** ../../graphiql-toolkit/esm/storage/query.js ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.QueryStore = void 0; +class QueryStore { + constructor(key, storage) { + let maxSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + this.key = key; + this.storage = storage; + this.maxSize = maxSize; + this.items = this.fetchAll(); + } + get length() { + return this.items.length; + } + contains(item) { + return this.items.some(x => x.query === item.query && x.variables === item.variables && x.headers === item.headers && x.operationName === item.operationName); + } + edit(item, index) { + if (typeof index === 'number' && this.items[index]) { + const found = this.items[index]; + if (found.query === item.query && found.variables === item.variables && found.headers === item.headers && found.operationName === item.operationName) { + this.items.splice(index, 1, item); + this.save(); + return; + } + } + const itemIndex = this.items.findIndex(x => x.query === item.query && x.variables === item.variables && x.headers === item.headers && x.operationName === item.operationName); + if (itemIndex !== -1) { + this.items.splice(itemIndex, 1, item); + this.save(); + } + } + delete(item) { + const itemIndex = this.items.findIndex(x => x.query === item.query && x.variables === item.variables && x.headers === item.headers && x.operationName === item.operationName); + if (itemIndex !== -1) { + this.items.splice(itemIndex, 1); + this.save(); + } + } + fetchRecent() { + return this.items.at(-1); + } + fetchAll() { + const raw = this.storage.get(this.key); + if (raw) { + return JSON.parse(raw)[this.key]; + } + return []; + } + push(item) { + const items = [...this.items, item]; + if (this.maxSize && items.length > this.maxSize) { + items.shift(); + } + for (let attempts = 0; attempts < 5; attempts++) { + const response = this.storage.set(this.key, JSON.stringify({ + [this.key]: items + })); + if (!(response === null || response === void 0 ? void 0 : response.error)) { + this.items = items; + } else if (response.isQuotaError && this.maxSize) { + items.shift(); + } else { + return; + } + } + } + save() { + this.storage.set(this.key, JSON.stringify({ + [this.key]: this.items + })); + } +} +exports.QueryStore = QueryStore; + +/***/ }), + +/***/ "./components/GraphiQL.tsx": +/*!*********************************!*\ + !*** ./components/GraphiQL.tsx ***! + \*********************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.GraphiQL = GraphiQL; +exports.GraphiQLInterface = GraphiQLInterface; +var _react = _interopRequireWildcard(__webpack_require__(/*! react */ "react")); +var _react2 = __webpack_require__(/*! @graphiql/react */ "../../graphiql-react/dist/index.js"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } +const majorVersion = parseInt(_react.default.version.slice(0, 2), 10); +if (majorVersion < 16) { + throw new Error(['GraphiQL 0.18.0 and after is not compatible with React 15 or below.', 'If you are using a CDN source (jsdelivr, unpkg, etc), follow this example:', 'https://github.com/graphql/graphiql/blob/master/examples/graphiql-cdn/index.html#L49'].join('\n')); +} +/** + * The top-level React component for GraphiQL, intended to encompass the entire + * browser viewport. + * + * @see https://github.com/graphql/graphiql#usage + */ + +function GraphiQL(_ref) { + let { + dangerouslyAssumeSchemaIsValid, + defaultQuery, + defaultTabs, + externalFragments, + fetcher, + getDefaultFieldNames, + headers, + inputValueDeprecation, + introspectionQueryName, + maxHistoryLength, + onEditOperationName, + onSchemaChange, + onTabChange, + onTogglePluginVisibility, + operationName, + plugins, + query, + response, + schema, + schemaDescription, + shouldPersistHeaders, + storage, + validationRules, + variables, + visiblePlugin, + defaultHeaders, + ...props + } = _ref; + // Ensure props are correct + if (typeof fetcher !== 'function') { + throw new TypeError('The `GraphiQL` component requires a `fetcher` function to be passed as prop.'); + } + return /*#__PURE__*/_react.default.createElement(_react2.GraphiQLProvider, { + getDefaultFieldNames: getDefaultFieldNames, + dangerouslyAssumeSchemaIsValid: dangerouslyAssumeSchemaIsValid, + defaultQuery: defaultQuery, + defaultHeaders: defaultHeaders, + defaultTabs: defaultTabs, + externalFragments: externalFragments, + fetcher: fetcher, + headers: headers, + inputValueDeprecation: inputValueDeprecation, + introspectionQueryName: introspectionQueryName, + maxHistoryLength: maxHistoryLength, + onEditOperationName: onEditOperationName, + onSchemaChange: onSchemaChange, + onTabChange: onTabChange, + onTogglePluginVisibility: onTogglePluginVisibility, + plugins: plugins, + visiblePlugin: visiblePlugin, + operationName: operationName, + query: query, + response: response, + schema: schema, + schemaDescription: schemaDescription, + shouldPersistHeaders: shouldPersistHeaders, + storage: storage, + validationRules: validationRules, + variables: variables + }, /*#__PURE__*/_react.default.createElement(GraphiQLInterface, _extends({ + showPersistHeadersSettings: shouldPersistHeaders !== false + }, props))); +} + +// Export main windows/panes to be used separately if desired. +GraphiQL.Logo = GraphiQLLogo; +GraphiQL.Toolbar = GraphiQLToolbar; +GraphiQL.Footer = GraphiQLFooter; +function GraphiQLInterface(props) { + var _props$isHeadersEdito, _pluginContext$visibl, _props$toolbar; + const isHeadersEditorEnabled = (_props$isHeadersEdito = props.isHeadersEditorEnabled) !== null && _props$isHeadersEdito !== void 0 ? _props$isHeadersEdito : true; + const editorContext = (0, _react2.useEditorContext)({ + nonNull: true + }); + const executionContext = (0, _react2.useExecutionContext)({ + nonNull: true + }); + const schemaContext = (0, _react2.useSchemaContext)({ + nonNull: true + }); + const storageContext = (0, _react2.useStorageContext)(); + const pluginContext = (0, _react2.usePluginContext)(); + const copy = (0, _react2.useCopyQuery)({ + onCopyQuery: props.onCopyQuery + }); + const merge = (0, _react2.useMergeQuery)(); + const prettify = (0, _react2.usePrettifyEditors)(); + const { + theme, + setTheme + } = (0, _react2.useTheme)(); + const PluginContent = pluginContext === null || pluginContext === void 0 ? void 0 : (_pluginContext$visibl = pluginContext.visiblePlugin) === null || _pluginContext$visibl === void 0 ? void 0 : _pluginContext$visibl.content; + const pluginResize = (0, _react2.useDragResize)({ + defaultSizeRelation: 1 / 3, + direction: 'horizontal', + initiallyHidden: pluginContext !== null && pluginContext !== void 0 && pluginContext.visiblePlugin ? undefined : 'first', + onHiddenElementChange(resizableElement) { + if (resizableElement === 'first') { + pluginContext === null || pluginContext === void 0 ? void 0 : pluginContext.setVisiblePlugin(null); + } + }, + sizeThresholdSecond: 200, + storageKey: 'docExplorerFlex' + }); + const editorResize = (0, _react2.useDragResize)({ + direction: 'horizontal', + storageKey: 'editorFlex' + }); + const editorToolsResize = (0, _react2.useDragResize)({ + defaultSizeRelation: 3, + direction: 'vertical', + initiallyHidden: (() => { + if (props.defaultEditorToolsVisibility === 'variables' || props.defaultEditorToolsVisibility === 'headers') { + return; + } + if (typeof props.defaultEditorToolsVisibility === 'boolean') { + return props.defaultEditorToolsVisibility ? undefined : 'second'; + } + return editorContext.initialVariables || editorContext.initialHeaders ? undefined : 'second'; + })(), + sizeThresholdSecond: 60, + storageKey: 'secondaryEditorFlex' + }); + const [activeSecondaryEditor, setActiveSecondaryEditor] = (0, _react.useState)(() => { + if (props.defaultEditorToolsVisibility === 'variables' || props.defaultEditorToolsVisibility === 'headers') { + return props.defaultEditorToolsVisibility; + } + return !editorContext.initialVariables && editorContext.initialHeaders && isHeadersEditorEnabled ? 'headers' : 'variables'; + }); + const [showDialog, setShowDialog] = (0, _react.useState)(null); + const [clearStorageStatus, setClearStorageStatus] = (0, _react.useState)(null); + const children = _react.default.Children.toArray(props.children); + const logo = children.find(child => isChildComponentType(child, GraphiQL.Logo)) || /*#__PURE__*/_react.default.createElement(GraphiQL.Logo, null); + const toolbar = children.find(child => isChildComponentType(child, GraphiQL.Toolbar)) || /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_react2.ToolbarButton, { + onClick: prettify, + label: "Prettify query (Shift-Ctrl-P)" + }, /*#__PURE__*/_react.default.createElement(_react2.PrettifyIcon, { + className: "graphiql-toolbar-icon", + "aria-hidden": "true" + })), /*#__PURE__*/_react.default.createElement(_react2.ToolbarButton, { + onClick: merge, + label: "Merge fragments into query (Shift-Ctrl-M)" + }, /*#__PURE__*/_react.default.createElement(_react2.MergeIcon, { + className: "graphiql-toolbar-icon", + "aria-hidden": "true" + })), /*#__PURE__*/_react.default.createElement(_react2.ToolbarButton, { + onClick: copy, + label: "Copy query (Shift-Ctrl-C)" + }, /*#__PURE__*/_react.default.createElement(_react2.CopyIcon, { + className: "graphiql-toolbar-icon", + "aria-hidden": "true" + })), (_props$toolbar = props.toolbar) === null || _props$toolbar === void 0 ? void 0 : _props$toolbar.additionalContent); + const footer = children.find(child => isChildComponentType(child, GraphiQL.Footer)); + const onClickReference = (0, _react.useCallback)(() => { + if (pluginResize.hiddenElement === 'first') { + pluginResize.setHiddenElement(null); + } + }, [pluginResize]); + const handleClearData = (0, _react.useCallback)(() => { + try { + storageContext === null || storageContext === void 0 ? void 0 : storageContext.clear(); + setClearStorageStatus('success'); + } catch { + setClearStorageStatus('error'); + } + }, [storageContext]); + const handlePersistHeaders = (0, _react.useCallback)(event => { + editorContext.setShouldPersistHeaders(event.currentTarget.dataset.value === 'true'); + }, [editorContext]); + const handleChangeTheme = (0, _react.useCallback)(event => { + const selectedTheme = event.currentTarget.dataset.theme; + setTheme(selectedTheme || null); + }, [setTheme]); + const handleAddTab = editorContext.addTab; + const handleRefetchSchema = schemaContext.introspect; + const handleReorder = editorContext.moveTab; + const handleShowDialog = (0, _react.useCallback)(event => { + setShowDialog(event.currentTarget.dataset.value); + }, []); + const handlePluginClick = (0, _react.useCallback)(e => { + const context = pluginContext; + const pluginIndex = Number(e.currentTarget.dataset.index); + const plugin = context.plugins.find((_, index) => pluginIndex === index); + const isVisible = plugin === context.visiblePlugin; + if (isVisible) { + context.setVisiblePlugin(null); + pluginResize.setHiddenElement('first'); + } else { + context.setVisiblePlugin(plugin); + pluginResize.setHiddenElement(null); + } + }, [pluginContext, pluginResize]); + const handleToolsTabClick = (0, _react.useCallback)(event => { + if (editorToolsResize.hiddenElement === 'second') { + editorToolsResize.setHiddenElement(null); + } + setActiveSecondaryEditor(event.currentTarget.dataset.name); + }, [editorToolsResize]); + const toggleEditorTools = (0, _react.useCallback)(() => { + editorToolsResize.setHiddenElement(editorToolsResize.hiddenElement === 'second' ? null : 'second'); + }, [editorToolsResize]); + const handleOpenShortKeysDialog = (0, _react.useCallback)(isOpen => { + if (!isOpen) { + setShowDialog(null); + } + }, []); + const handleOpenSettingsDialog = (0, _react.useCallback)(isOpen => { + if (!isOpen) { + setShowDialog(null); + setClearStorageStatus(null); + } + }, []); + const addTab = /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + label: "Add tab" + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + className: "graphiql-tab-add", + onClick: handleAddTab, + "aria-label": "Add tab" + }, /*#__PURE__*/_react.default.createElement(_react2.PlusIcon, { + "aria-hidden": "true" + }))); + return /*#__PURE__*/_react.default.createElement(_react2.Tooltip.Provider, null, /*#__PURE__*/_react.default.createElement("div", { + "data-testid": "graphiql-container", + className: "graphiql-container" + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-sidebar" + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-sidebar-section" + }, pluginContext === null || pluginContext === void 0 ? void 0 : pluginContext.plugins.map((plugin, index) => { + const isVisible = plugin === pluginContext.visiblePlugin; + const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; + const Icon = plugin.icon; + return /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + key: plugin.title, + label: label + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + className: isVisible ? 'active' : '', + onClick: handlePluginClick, + "data-index": index, + "aria-label": label + }, /*#__PURE__*/_react.default.createElement(Icon, { + "aria-hidden": "true" + }))); + })), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-sidebar-section" + }, /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + label: "Re-fetch GraphQL schema" + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + disabled: schemaContext.isFetching, + onClick: handleRefetchSchema, + "aria-label": "Re-fetch GraphQL schema" + }, /*#__PURE__*/_react.default.createElement(_react2.ReloadIcon, { + className: schemaContext.isFetching ? 'graphiql-spin' : '', + "aria-hidden": "true" + }))), /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + label: "Open short keys dialog" + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + "data-value": "short-keys", + onClick: handleShowDialog, + "aria-label": "Open short keys dialog" + }, /*#__PURE__*/_react.default.createElement(_react2.KeyboardShortcutIcon, { + "aria-hidden": "true" + }))), /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + label: "Open settings dialog" + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + "data-value": "settings", + onClick: handleShowDialog, + "aria-label": "Open settings dialog" + }, /*#__PURE__*/_react.default.createElement(_react2.SettingsIcon, { + "aria-hidden": "true" + }))))), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-main" + }, /*#__PURE__*/_react.default.createElement("div", { + ref: pluginResize.firstRef, + style: { + // Make sure the container shrinks when containing long + // non-breaking texts + minWidth: '200px' + } + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-plugin" + }, PluginContent ? /*#__PURE__*/_react.default.createElement(PluginContent, null) : null)), (pluginContext === null || pluginContext === void 0 ? void 0 : pluginContext.visiblePlugin) && /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-horizontal-drag-bar", + ref: pluginResize.dragBarRef + }), /*#__PURE__*/_react.default.createElement("div", { + ref: pluginResize.secondRef, + className: "graphiql-sessions" + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-session-header" + }, /*#__PURE__*/_react.default.createElement(_react2.Tabs, { + values: editorContext.tabs, + onReorder: handleReorder, + "aria-label": "Select active operation" + }, editorContext.tabs.length > 1 && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, editorContext.tabs.map((tab, index) => /*#__PURE__*/_react.default.createElement(_react2.Tab, { + key: tab.id, + value: tab, + isActive: index === editorContext.activeTabIndex + }, /*#__PURE__*/_react.default.createElement(_react2.Tab.Button, { + "aria-controls": "graphiql-session", + id: `graphiql-session-tab-${index}`, + onClick: () => { + executionContext.stop(); + editorContext.changeTab(index); + } + }, tab.title), /*#__PURE__*/_react.default.createElement(_react2.Tab.Close, { + onClick: () => { + if (editorContext.activeTabIndex === index) { + executionContext.stop(); + } + editorContext.closeTab(index); + } + }))), addTab)), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-session-header-right" + }, editorContext.tabs.length === 1 && addTab, logo)), /*#__PURE__*/_react.default.createElement("div", { + role: "tabpanel", + id: "graphiql-session", + className: "graphiql-session", + "aria-labelledby": `graphiql-session-tab-${editorContext.activeTabIndex}` + }, /*#__PURE__*/_react.default.createElement("div", { + ref: editorResize.firstRef + }, /*#__PURE__*/_react.default.createElement("div", { + className: `graphiql-editors${editorContext.tabs.length === 1 ? ' full-height' : ''}` + }, /*#__PURE__*/_react.default.createElement("div", { + ref: editorToolsResize.firstRef + }, /*#__PURE__*/_react.default.createElement("section", { + className: "graphiql-query-editor", + "aria-label": "Query Editor" + }, /*#__PURE__*/_react.default.createElement(_react2.QueryEditor, { + editorTheme: props.editorTheme, + keyMap: props.keyMap, + onClickReference: onClickReference, + onCopyQuery: props.onCopyQuery, + onEdit: props.onEditQuery, + readOnly: props.readOnly + }), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-toolbar", + role: "toolbar", + "aria-label": "Editor Commands" + }, /*#__PURE__*/_react.default.createElement(_react2.ExecuteButton, null), toolbar))), /*#__PURE__*/_react.default.createElement("div", { + ref: editorToolsResize.dragBarRef + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-editor-tools" + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + className: activeSecondaryEditor === 'variables' && editorToolsResize.hiddenElement !== 'second' ? 'active' : '', + onClick: handleToolsTabClick, + "data-name": "variables" + }, "Variables"), isHeadersEditorEnabled && /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + className: activeSecondaryEditor === 'headers' && editorToolsResize.hiddenElement !== 'second' ? 'active' : '', + onClick: handleToolsTabClick, + "data-name": "headers" + }, "Headers"), /*#__PURE__*/_react.default.createElement(_react2.Tooltip, { + label: editorToolsResize.hiddenElement === 'second' ? 'Show editor tools' : 'Hide editor tools' + }, /*#__PURE__*/_react.default.createElement(_react2.UnStyledButton, { + type: "button", + onClick: toggleEditorTools, + "aria-label": editorToolsResize.hiddenElement === 'second' ? 'Show editor tools' : 'Hide editor tools', + className: "graphiql-toggle-editor-tools" + }, editorToolsResize.hiddenElement === 'second' ? /*#__PURE__*/_react.default.createElement(_react2.ChevronUpIcon, { + className: "graphiql-chevron-icon", + "aria-hidden": "true" + }) : /*#__PURE__*/_react.default.createElement(_react2.ChevronDownIcon, { + className: "graphiql-chevron-icon", + "aria-hidden": "true" + }))))), /*#__PURE__*/_react.default.createElement("div", { + ref: editorToolsResize.secondRef + }, /*#__PURE__*/_react.default.createElement("section", { + className: "graphiql-editor-tool", + "aria-label": activeSecondaryEditor === 'variables' ? 'Variables' : 'Headers' + }, /*#__PURE__*/_react.default.createElement(_react2.VariableEditor, { + editorTheme: props.editorTheme, + isHidden: activeSecondaryEditor !== 'variables', + keyMap: props.keyMap, + onEdit: props.onEditVariables, + onClickReference: onClickReference, + readOnly: props.readOnly + }), isHeadersEditorEnabled && /*#__PURE__*/_react.default.createElement(_react2.HeaderEditor, { + editorTheme: props.editorTheme, + isHidden: activeSecondaryEditor !== 'headers', + keyMap: props.keyMap, + onEdit: props.onEditHeaders, + readOnly: props.readOnly + }))))), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-horizontal-drag-bar", + ref: editorResize.dragBarRef + }), /*#__PURE__*/_react.default.createElement("div", { + ref: editorResize.secondRef + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-response" + }, executionContext.isFetching ? /*#__PURE__*/_react.default.createElement(_react2.Spinner, null) : null, /*#__PURE__*/_react.default.createElement(_react2.ResponseEditor, { + editorTheme: props.editorTheme, + responseTooltip: props.responseTooltip, + keyMap: props.keyMap + }), footer))))), /*#__PURE__*/_react.default.createElement(_react2.Dialog, { + open: showDialog === 'short-keys', + onOpenChange: handleOpenShortKeysDialog + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-header" + }, /*#__PURE__*/_react.default.createElement(_react2.Dialog.Title, { + className: "graphiql-dialog-title" + }, "Short Keys"), /*#__PURE__*/_react.default.createElement(_react2.Dialog.Close, null)), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section" + }, /*#__PURE__*/_react.default.createElement(ShortKeys, { + keyMap: props.keyMap || 'sublime' + }))), /*#__PURE__*/_react.default.createElement(_react2.Dialog, { + open: showDialog === 'settings', + onOpenChange: handleOpenSettingsDialog + }, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-header" + }, /*#__PURE__*/_react.default.createElement(_react2.Dialog.Title, { + className: "graphiql-dialog-title" + }, "Settings"), /*#__PURE__*/_react.default.createElement(_react2.Dialog.Close, null)), props.showPersistHeadersSettings ? /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section" + }, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-title" + }, "Persist headers"), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-caption" + }, "Save headers upon reloading.", ' ', /*#__PURE__*/_react.default.createElement("span", { + className: "graphiql-warning-text" + }, "Only enable if you trust this device."))), /*#__PURE__*/_react.default.createElement(_react2.ButtonGroup, null, /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + id: "enable-persist-headers", + className: editorContext.shouldPersistHeaders ? 'active' : '', + "data-value": "true", + onClick: handlePersistHeaders + }, "On"), /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + id: "disable-persist-headers", + className: editorContext.shouldPersistHeaders ? '' : 'active', + onClick: handlePersistHeaders + }, "Off"))) : null, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section" + }, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-title" + }, "Theme"), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-caption" + }, "Adjust how the interface looks like.")), /*#__PURE__*/_react.default.createElement(_react2.ButtonGroup, null, /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + className: theme === null ? 'active' : '', + onClick: handleChangeTheme + }, "System"), /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + className: theme === 'light' ? 'active' : '', + "data-theme": "light", + onClick: handleChangeTheme + }, "Light"), /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + className: theme === 'dark' ? 'active' : '', + "data-theme": "dark", + onClick: handleChangeTheme + }, "Dark"))), storageContext ? /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section" + }, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-title" + }, "Clear storage"), /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-dialog-section-caption" + }, "Remove all locally stored data and start fresh.")), /*#__PURE__*/_react.default.createElement(_react2.Button, { + type: "button", + state: clearStorageStatus || undefined, + disabled: clearStorageStatus === 'success', + onClick: handleClearData + }, { + success: 'Cleared data', + error: 'Failed' + }[clearStorageStatus] || 'Clear data')) : null))); +} +const modifier = typeof window !== 'undefined' && window.navigator.platform.toLowerCase().indexOf('mac') === 0 ? 'Cmd' : 'Ctrl'; +const SHORT_KEYS = Object.entries({ + 'Search in editor': [modifier, 'F'], + 'Search in documentation': [modifier, 'K'], + 'Execute query': [modifier, 'Enter'], + 'Prettify editors': ['Ctrl', 'Shift', 'P'], + 'Merge fragments definitions into operation definition': ['Ctrl', 'Shift', 'M'], + 'Copy query': ['Ctrl', 'Shift', 'C'], + 'Re-fetch schema using introspection': ['Ctrl', 'Shift', 'R'] +}); +function ShortKeys(_ref2) { + let { + keyMap + } = _ref2; + return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("table", { + className: "graphiql-table" + }, /*#__PURE__*/_react.default.createElement("thead", null, /*#__PURE__*/_react.default.createElement("tr", null, /*#__PURE__*/_react.default.createElement("th", null, "Short Key"), /*#__PURE__*/_react.default.createElement("th", null, "Function"))), /*#__PURE__*/_react.default.createElement("tbody", null, SHORT_KEYS.map(_ref3 => { + let [title, keys] = _ref3; + return /*#__PURE__*/_react.default.createElement("tr", { + key: title + }, /*#__PURE__*/_react.default.createElement("td", null, keys.map((key, index, array) => /*#__PURE__*/_react.default.createElement(_react.Fragment, { + key: key + }, /*#__PURE__*/_react.default.createElement("code", { + className: "graphiql-key" + }, key), index !== array.length - 1 && ' + '))), /*#__PURE__*/_react.default.createElement("td", null, title)); + }))), /*#__PURE__*/_react.default.createElement("p", null, "The editors use", ' ', /*#__PURE__*/_react.default.createElement("a", { + href: "https://codemirror.net/5/doc/manual.html#keymaps", + target: "_blank", + rel: "noopener noreferrer" + }, "CodeMirror Key Maps"), ' ', "that add more short keys. This instance of Graph", /*#__PURE__*/_react.default.createElement("em", null, "i"), "QL uses", ' ', /*#__PURE__*/_react.default.createElement("code", null, keyMap), ".")); +} + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLLogo(props) { + return /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-logo" + }, props.children || /*#__PURE__*/_react.default.createElement("a", { + className: "graphiql-logo-link", + href: "https://github.com/graphql/graphiql", + target: "_blank", + rel: "noreferrer" + }, "Graph", /*#__PURE__*/_react.default.createElement("em", null, "i"), "QL")); +} +GraphiQLLogo.displayName = 'GraphiQLLogo'; + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLToolbar(props) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, props.children); +} +GraphiQLToolbar.displayName = 'GraphiQLToolbar'; + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLFooter(props) { + return /*#__PURE__*/_react.default.createElement("div", { + className: "graphiql-footer" + }, props.children); +} +GraphiQLFooter.displayName = 'GraphiQLFooter'; + +// Determines if the React child is of the same type of the provided React component +function isChildComponentType(child, component) { + var _child$type; + if (child !== null && child !== void 0 && (_child$type = child.type) !== null && _child$type !== void 0 && _child$type.displayName && child.type.displayName === component.displayName) { + return true; + } + return child.type === component; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/index.js": +/*!***************************************************!*\ + !*** ../../graphql-language-service/esm/index.js ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "CharacterStream", ({ + enumerable: true, + get: function () { + return _parser.CharacterStream; + } +})); +Object.defineProperty(exports, "CompletionItemKind", ({ + enumerable: true, + get: function () { + return _types.CompletionItemKind; + } +})); +Object.defineProperty(exports, "DIAGNOSTIC_SEVERITY", ({ + enumerable: true, + get: function () { + return _interface.DIAGNOSTIC_SEVERITY; + } +})); +Object.defineProperty(exports, "FileChangeTypeKind", ({ + enumerable: true, + get: function () { + return _types.FileChangeTypeKind; + } +})); +Object.defineProperty(exports, "LexRules", ({ + enumerable: true, + get: function () { + return _parser.LexRules; + } +})); +Object.defineProperty(exports, "ParseRules", ({ + enumerable: true, + get: function () { + return _parser.ParseRules; + } +})); +Object.defineProperty(exports, "Position", ({ + enumerable: true, + get: function () { + return _utils.Position; + } +})); +Object.defineProperty(exports, "Range", ({ + enumerable: true, + get: function () { + return _utils.Range; + } +})); +Object.defineProperty(exports, "RuleKinds", ({ + enumerable: true, + get: function () { + return _parser.RuleKinds; + } +})); +Object.defineProperty(exports, "SEVERITY", ({ + enumerable: true, + get: function () { + return _interface.SEVERITY; + } +})); +Object.defineProperty(exports, "SuggestionCommand", ({ + enumerable: true, + get: function () { + return _interface.SuggestionCommand; + } +})); +Object.defineProperty(exports, "canUseDirective", ({ + enumerable: true, + get: function () { + return _interface.canUseDirective; + } +})); +Object.defineProperty(exports, "collectVariables", ({ + enumerable: true, + get: function () { + return _utils.collectVariables; + } +})); +Object.defineProperty(exports, "getASTNodeAtPosition", ({ + enumerable: true, + get: function () { + return _utils.getASTNodeAtPosition; + } +})); +Object.defineProperty(exports, "getAutocompleteSuggestions", ({ + enumerable: true, + get: function () { + return _interface.getAutocompleteSuggestions; + } +})); +Object.defineProperty(exports, "getDefinitionQueryResultForDefinitionNode", ({ + enumerable: true, + get: function () { + return _interface.getDefinitionQueryResultForDefinitionNode; + } +})); +Object.defineProperty(exports, "getDefinitionQueryResultForField", ({ + enumerable: true, + get: function () { + return _interface.getDefinitionQueryResultForField; + } +})); +Object.defineProperty(exports, "getDefinitionQueryResultForFragmentSpread", ({ + enumerable: true, + get: function () { + return _interface.getDefinitionQueryResultForFragmentSpread; + } +})); +Object.defineProperty(exports, "getDefinitionQueryResultForNamedType", ({ + enumerable: true, + get: function () { + return _interface.getDefinitionQueryResultForNamedType; + } +})); +Object.defineProperty(exports, "getDefinitionState", ({ + enumerable: true, + get: function () { + return _interface.getDefinitionState; + } +})); +Object.defineProperty(exports, "getDiagnostics", ({ + enumerable: true, + get: function () { + return _interface.getDiagnostics; + } +})); +Object.defineProperty(exports, "getFieldDef", ({ + enumerable: true, + get: function () { + return _interface.getFieldDef; + } +})); +Object.defineProperty(exports, "getFragmentDefinitions", ({ + enumerable: true, + get: function () { + return _interface.getFragmentDefinitions; + } +})); +Object.defineProperty(exports, "getFragmentDependencies", ({ + enumerable: true, + get: function () { + return _utils.getFragmentDependencies; + } +})); +Object.defineProperty(exports, "getFragmentDependenciesForAST", ({ + enumerable: true, + get: function () { + return _utils.getFragmentDependenciesForAST; + } +})); +Object.defineProperty(exports, "getHoverInformation", ({ + enumerable: true, + get: function () { + return _interface.getHoverInformation; + } +})); +Object.defineProperty(exports, "getOperationASTFacts", ({ + enumerable: true, + get: function () { + return _utils.getOperationASTFacts; + } +})); +Object.defineProperty(exports, "getOperationFacts", ({ + enumerable: true, + get: function () { + return _utils.getOperationFacts; + } +})); +Object.defineProperty(exports, "getOutline", ({ + enumerable: true, + get: function () { + return _interface.getOutline; + } +})); +Object.defineProperty(exports, "getQueryFacts", ({ + enumerable: true, + get: function () { + return _utils.getQueryFacts; + } +})); +Object.defineProperty(exports, "getRange", ({ + enumerable: true, + get: function () { + return _interface.getRange; + } +})); +Object.defineProperty(exports, "getTokenAtPosition", ({ + enumerable: true, + get: function () { + return _interface.getTokenAtPosition; + } +})); +Object.defineProperty(exports, "getTypeInfo", ({ + enumerable: true, + get: function () { + return _interface.getTypeInfo; + } +})); +Object.defineProperty(exports, "getVariableCompletions", ({ + enumerable: true, + get: function () { + return _interface.getVariableCompletions; + } +})); +Object.defineProperty(exports, "getVariablesJSONSchema", ({ + enumerable: true, + get: function () { + return _utils.getVariablesJSONSchema; + } +})); +Object.defineProperty(exports, "isIgnored", ({ + enumerable: true, + get: function () { + return _parser.isIgnored; + } +})); +Object.defineProperty(exports, "list", ({ + enumerable: true, + get: function () { + return _parser.list; + } +})); +Object.defineProperty(exports, "offsetToPosition", ({ + enumerable: true, + get: function () { + return _utils.offsetToPosition; + } +})); +Object.defineProperty(exports, "onlineParser", ({ + enumerable: true, + get: function () { + return _parser.onlineParser; + } +})); +Object.defineProperty(exports, "opt", ({ + enumerable: true, + get: function () { + return _parser.opt; + } +})); +Object.defineProperty(exports, "p", ({ + enumerable: true, + get: function () { + return _parser.p; + } +})); +Object.defineProperty(exports, "pointToOffset", ({ + enumerable: true, + get: function () { + return _utils.pointToOffset; + } +})); +Object.defineProperty(exports, "t", ({ + enumerable: true, + get: function () { + return _parser.t; + } +})); +Object.defineProperty(exports, "validateQuery", ({ + enumerable: true, + get: function () { + return _interface.validateQuery; + } +})); +Object.defineProperty(exports, "validateWithCustomRules", ({ + enumerable: true, + get: function () { + return _utils.validateWithCustomRules; + } +})); +var _interface = __webpack_require__(/*! ./interface */ "../../graphql-language-service/esm/interface/index.js"); +var _parser = __webpack_require__(/*! ./parser */ "../../graphql-language-service/esm/parser/index.js"); +var _types = __webpack_require__(/*! ./types */ "../../graphql-language-service/esm/types.js"); +var _utils = __webpack_require__(/*! ./utils */ "../../graphql-language-service/esm/utils/index.js"); + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/autocompleteUtils.js": +/*!*************************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/autocompleteUtils.js ***! + \*************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.forEachState = forEachState; +exports.getDefinitionState = getDefinitionState; +exports.getFieldDef = getFieldDef; +exports.hintList = hintList; +exports.objectValues = objectValues; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function getDefinitionState(tokenState) { + let definitionState; + forEachState(tokenState, state => { + switch (state.kind) { + case 'Query': + case 'ShortQuery': + case 'Mutation': + case 'Subscription': + case 'FragmentDefinition': + definitionState = state; + break; + } + }); + return definitionState; +} +function getFieldDef(schema, type, fieldName) { + if (fieldName === _graphql.SchemaMetaFieldDef.name && schema.getQueryType() === type) { + return _graphql.SchemaMetaFieldDef; + } + if (fieldName === _graphql.TypeMetaFieldDef.name && schema.getQueryType() === type) { + return _graphql.TypeMetaFieldDef; + } + if (fieldName === _graphql.TypeNameMetaFieldDef.name && (0, _graphql.isCompositeType)(type)) { + return _graphql.TypeNameMetaFieldDef; + } + if ('getFields' in type) { + return type.getFields()[fieldName]; + } + return null; +} +function forEachState(stack, fn) { + const reverseStateStack = []; + let state = stack; + while (state === null || state === void 0 ? void 0 : state.kind) { + reverseStateStack.push(state); + state = state.prevState; + } + for (let i = reverseStateStack.length - 1; i >= 0; i--) { + fn(reverseStateStack[i]); + } +} +function objectValues(object) { + const keys = Object.keys(object); + const len = keys.length; + const values = new Array(len); + for (let i = 0; i < len; ++i) { + values[i] = object[keys[i]]; + } + return values; +} +function hintList(token, list) { + return filterAndSortList(list, normalizeText(token.string)); +} +function filterAndSortList(list, text) { + if (!text) { + return filterNonEmpty(list, entry => !entry.isDeprecated); + } + const byProximity = list.map(entry => ({ + proximity: getProximity(normalizeText(entry.label), text), + entry + })); + return filterNonEmpty(filterNonEmpty(byProximity, pair => pair.proximity <= 2), pair => !pair.entry.isDeprecated).sort((a, b) => (a.entry.isDeprecated ? 1 : 0) - (b.entry.isDeprecated ? 1 : 0) || a.proximity - b.proximity || a.entry.label.length - b.entry.label.length).map(pair => pair.entry); +} +function filterNonEmpty(array, predicate) { + const filtered = array.filter(predicate); + return filtered.length === 0 ? array : filtered; +} +function normalizeText(text) { + return text.toLowerCase().replaceAll(/\W/g, ''); +} +function getProximity(suggestion, text) { + let proximity = lexicalDistance(text, suggestion); + if (suggestion.length > text.length) { + proximity -= suggestion.length - text.length - 1; + proximity += suggestion.indexOf(text) === 0 ? 0 : 0.5; + } + return proximity; +} +function lexicalDistance(a, b) { + let i; + let j; + const d = []; + const aLength = a.length; + const bLength = b.length; + for (i = 0; i <= aLength; i++) { + d[i] = [i]; + } + for (j = 1; j <= bLength; j++) { + d[0][j] = j; + } + for (i = 1; i <= aLength; i++) { + for (j = 1; j <= bLength; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); + if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) { + d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); + } + } + } + return d[aLength][bLength]; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/getAutocompleteSuggestions.js": +/*!**********************************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/getAutocompleteSuggestions.js ***! + \**********************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SuggestionCommand = exports.GraphQLDocumentMode = void 0; +exports.canUseDirective = canUseDirective; +exports.getAutocompleteSuggestions = getAutocompleteSuggestions; +exports.getFragmentDefinitions = getFragmentDefinitions; +exports.getTokenAtPosition = getTokenAtPosition; +exports.getTypeInfo = getTypeInfo; +exports.getVariableCompletions = getVariableCompletions; +exports.runOnlineParser = runOnlineParser; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _types = __webpack_require__(/*! ../types */ "../../graphql-language-service/esm/types.js"); +var _parser = __webpack_require__(/*! ../parser */ "../../graphql-language-service/esm/parser/index.js"); +var _autocompleteUtils = __webpack_require__(/*! ./autocompleteUtils */ "../../graphql-language-service/esm/interface/autocompleteUtils.js"); +const SuggestionCommand = { + command: 'editor.action.triggerSuggest', + title: 'Suggestions' +}; +exports.SuggestionCommand = SuggestionCommand; +const collectFragmentDefs = op => { + const externalFragments = []; + if (op) { + try { + (0, _graphql.visit)((0, _graphql.parse)(op), { + FragmentDefinition(def) { + externalFragments.push(def); + } + }); + } catch (_a) { + return []; + } + } + return externalFragments; +}; +const typeSystemKinds = [_graphql.Kind.SCHEMA_DEFINITION, _graphql.Kind.OPERATION_TYPE_DEFINITION, _graphql.Kind.SCALAR_TYPE_DEFINITION, _graphql.Kind.OBJECT_TYPE_DEFINITION, _graphql.Kind.INTERFACE_TYPE_DEFINITION, _graphql.Kind.UNION_TYPE_DEFINITION, _graphql.Kind.ENUM_TYPE_DEFINITION, _graphql.Kind.INPUT_OBJECT_TYPE_DEFINITION, _graphql.Kind.DIRECTIVE_DEFINITION, _graphql.Kind.SCHEMA_EXTENSION, _graphql.Kind.SCALAR_TYPE_EXTENSION, _graphql.Kind.OBJECT_TYPE_EXTENSION, _graphql.Kind.INTERFACE_TYPE_EXTENSION, _graphql.Kind.UNION_TYPE_EXTENSION, _graphql.Kind.ENUM_TYPE_EXTENSION, _graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION]; +const hasTypeSystemDefinitions = sdl => { + let hasTypeSystemDef = false; + if (sdl) { + try { + (0, _graphql.visit)((0, _graphql.parse)(sdl), { + enter(node) { + if (node.kind === 'Document') { + return; + } + if (typeSystemKinds.includes(node.kind)) { + hasTypeSystemDef = true; + return _graphql.BREAK; + } + return false; + } + }); + } catch (_a) { + return hasTypeSystemDef; + } + } + return hasTypeSystemDef; +}; +function getAutocompleteSuggestions(schema, queryText, cursor, contextToken, fragmentDefs, options) { + var _a; + const opts = Object.assign(Object.assign({}, options), { + schema + }); + const token = contextToken || getTokenAtPosition(queryText, cursor, 1); + const state = token.state.kind === 'Invalid' ? token.state.prevState : token.state; + const mode = (options === null || options === void 0 ? void 0 : options.mode) || getDocumentMode(queryText, options === null || options === void 0 ? void 0 : options.uri); + if (!state) { + return []; + } + const { + kind, + step, + prevState + } = state; + const typeInfo = getTypeInfo(schema, token.state); + if (kind === _parser.RuleKinds.DOCUMENT) { + if (mode === GraphQLDocumentMode.TYPE_SYSTEM) { + return getSuggestionsForTypeSystemDefinitions(token); + } + return getSuggestionsForExecutableDefinitions(token); + } + if (kind === _parser.RuleKinds.EXTEND_DEF) { + return getSuggestionsForExtensionDefinitions(token); + } + if (((_a = prevState === null || prevState === void 0 ? void 0 : prevState.prevState) === null || _a === void 0 ? void 0 : _a.kind) === _parser.RuleKinds.EXTENSION_DEFINITION && state.name) { + return (0, _autocompleteUtils.hintList)(token, []); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.SCALAR_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(_graphql.isScalarType).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.OBJECT_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(type => (0, _graphql.isObjectType)(type) && !type.name.startsWith('__')).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.INTERFACE_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(_graphql.isInterfaceType).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.UNION_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(_graphql.isUnionType).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.ENUM_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(type => (0, _graphql.isEnumType)(type) && !type.name.startsWith('__')).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if ((prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(_graphql.isInputObjectType).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if (kind === _parser.RuleKinds.IMPLEMENTS || kind === _parser.RuleKinds.NAMED_TYPE && (prevState === null || prevState === void 0 ? void 0 : prevState.kind) === _parser.RuleKinds.IMPLEMENTS) { + return getSuggestionsForImplements(token, state, schema, queryText, typeInfo); + } + if (kind === _parser.RuleKinds.SELECTION_SET || kind === _parser.RuleKinds.FIELD || kind === _parser.RuleKinds.ALIASED_FIELD) { + return getSuggestionsForFieldNames(token, typeInfo, opts); + } + if (kind === _parser.RuleKinds.ARGUMENTS || kind === _parser.RuleKinds.ARGUMENT && step === 0) { + const { + argDefs + } = typeInfo; + if (argDefs) { + return (0, _autocompleteUtils.hintList)(token, argDefs.map(argDef => { + var _a; + return { + label: argDef.name, + insertText: argDef.name + ': ', + command: SuggestionCommand, + detail: String(argDef.type), + documentation: (_a = argDef.description) !== null && _a !== void 0 ? _a : undefined, + kind: _types.CompletionItemKind.Variable, + type: argDef.type + }; + })); + } + } + if ((kind === _parser.RuleKinds.OBJECT_VALUE || kind === _parser.RuleKinds.OBJECT_FIELD && step === 0) && typeInfo.objectFieldDefs) { + const objectFields = (0, _autocompleteUtils.objectValues)(typeInfo.objectFieldDefs); + const completionKind = kind === _parser.RuleKinds.OBJECT_VALUE ? _types.CompletionItemKind.Value : _types.CompletionItemKind.Field; + return (0, _autocompleteUtils.hintList)(token, objectFields.map(field => { + var _a; + return { + label: field.name, + detail: String(field.type), + documentation: (_a = field.description) !== null && _a !== void 0 ? _a : undefined, + kind: completionKind, + type: field.type + }; + })); + } + if (kind === _parser.RuleKinds.ENUM_VALUE || kind === _parser.RuleKinds.LIST_VALUE && step === 1 || kind === _parser.RuleKinds.OBJECT_FIELD && step === 2 || kind === _parser.RuleKinds.ARGUMENT && step === 2) { + return getSuggestionsForInputValues(token, typeInfo, queryText, schema); + } + if (kind === _parser.RuleKinds.VARIABLE && step === 1) { + const namedInputType = (0, _graphql.getNamedType)(typeInfo.inputType); + const variableDefinitions = getVariableCompletions(queryText, schema, token); + return (0, _autocompleteUtils.hintList)(token, variableDefinitions.filter(v => v.detail === (namedInputType === null || namedInputType === void 0 ? void 0 : namedInputType.name))); + } + if (kind === _parser.RuleKinds.TYPE_CONDITION && step === 1 || kind === _parser.RuleKinds.NAMED_TYPE && prevState != null && prevState.kind === _parser.RuleKinds.TYPE_CONDITION) { + return getSuggestionsForFragmentTypeConditions(token, typeInfo, schema, kind); + } + if (kind === _parser.RuleKinds.FRAGMENT_SPREAD && step === 1) { + return getSuggestionsForFragmentSpread(token, typeInfo, schema, queryText, Array.isArray(fragmentDefs) ? fragmentDefs : collectFragmentDefs(fragmentDefs)); + } + const unwrappedState = unwrapType(state); + if (mode === GraphQLDocumentMode.TYPE_SYSTEM && !unwrappedState.needsAdvance && kind === _parser.RuleKinds.NAMED_TYPE || kind === _parser.RuleKinds.LIST_TYPE) { + if (unwrappedState.kind === _parser.RuleKinds.FIELD_DEF) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(type => (0, _graphql.isOutputType)(type) && !type.name.startsWith('__')).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + if (unwrappedState.kind === _parser.RuleKinds.INPUT_VALUE_DEF) { + return (0, _autocompleteUtils.hintList)(token, Object.values(schema.getTypeMap()).filter(type => (0, _graphql.isInputType)(type) && !type.name.startsWith('__')).map(type => ({ + label: type.name, + kind: _types.CompletionItemKind.Function + }))); + } + } + if (kind === _parser.RuleKinds.VARIABLE_DEFINITION && step === 2 || kind === _parser.RuleKinds.LIST_TYPE && step === 1 || kind === _parser.RuleKinds.NAMED_TYPE && prevState && (prevState.kind === _parser.RuleKinds.VARIABLE_DEFINITION || prevState.kind === _parser.RuleKinds.LIST_TYPE || prevState.kind === _parser.RuleKinds.NON_NULL_TYPE)) { + return getSuggestionsForVariableDefinition(token, schema, kind); + } + if (kind === _parser.RuleKinds.DIRECTIVE) { + return getSuggestionsForDirective(token, state, schema, kind); + } + return []; +} +const insertSuffix = ' {\n $1\n}'; +const getInsertText = field => { + const { + type + } = field; + if ((0, _graphql.isCompositeType)(type)) { + return insertSuffix; + } + if ((0, _graphql.isListType)(type) && (0, _graphql.isCompositeType)(type.ofType)) { + return insertSuffix; + } + if ((0, _graphql.isNonNullType)(type)) { + if ((0, _graphql.isCompositeType)(type.ofType)) { + return insertSuffix; + } + if ((0, _graphql.isListType)(type.ofType) && (0, _graphql.isCompositeType)(type.ofType.ofType)) { + return insertSuffix; + } + } + return null; +}; +function getSuggestionsForTypeSystemDefinitions(token) { + return (0, _autocompleteUtils.hintList)(token, [{ + label: 'extend', + kind: _types.CompletionItemKind.Function + }, { + label: 'type', + kind: _types.CompletionItemKind.Function + }, { + label: 'interface', + kind: _types.CompletionItemKind.Function + }, { + label: 'union', + kind: _types.CompletionItemKind.Function + }, { + label: 'input', + kind: _types.CompletionItemKind.Function + }, { + label: 'scalar', + kind: _types.CompletionItemKind.Function + }, { + label: 'schema', + kind: _types.CompletionItemKind.Function + }]); +} +function getSuggestionsForExecutableDefinitions(token) { + return (0, _autocompleteUtils.hintList)(token, [{ + label: 'query', + kind: _types.CompletionItemKind.Function + }, { + label: 'mutation', + kind: _types.CompletionItemKind.Function + }, { + label: 'subscription', + kind: _types.CompletionItemKind.Function + }, { + label: 'fragment', + kind: _types.CompletionItemKind.Function + }, { + label: '{', + kind: _types.CompletionItemKind.Constructor + }]); +} +function getSuggestionsForExtensionDefinitions(token) { + return (0, _autocompleteUtils.hintList)(token, [{ + label: 'type', + kind: _types.CompletionItemKind.Function + }, { + label: 'interface', + kind: _types.CompletionItemKind.Function + }, { + label: 'union', + kind: _types.CompletionItemKind.Function + }, { + label: 'input', + kind: _types.CompletionItemKind.Function + }, { + label: 'scalar', + kind: _types.CompletionItemKind.Function + }, { + label: 'schema', + kind: _types.CompletionItemKind.Function + }]); +} +function getSuggestionsForFieldNames(token, typeInfo, options) { + var _a; + if (typeInfo.parentType) { + const { + parentType + } = typeInfo; + let fields = []; + if ('getFields' in parentType) { + fields = (0, _autocompleteUtils.objectValues)(parentType.getFields()); + } + if ((0, _graphql.isCompositeType)(parentType)) { + fields.push(_graphql.TypeNameMetaFieldDef); + } + if (parentType === ((_a = options === null || options === void 0 ? void 0 : options.schema) === null || _a === void 0 ? void 0 : _a.getQueryType())) { + fields.push(_graphql.SchemaMetaFieldDef, _graphql.TypeMetaFieldDef); + } + return (0, _autocompleteUtils.hintList)(token, fields.map((field, index) => { + var _a; + const suggestion = { + sortText: String(index) + field.name, + label: field.name, + detail: String(field.type), + documentation: (_a = field.description) !== null && _a !== void 0 ? _a : undefined, + deprecated: Boolean(field.deprecationReason), + isDeprecated: Boolean(field.deprecationReason), + deprecationReason: field.deprecationReason, + kind: _types.CompletionItemKind.Field, + type: field.type + }; + if (options === null || options === void 0 ? void 0 : options.fillLeafsOnComplete) { + const insertText = getInsertText(field); + if (insertText) { + suggestion.insertText = field.name + insertText; + suggestion.insertTextFormat = _types.InsertTextFormat.Snippet; + suggestion.command = SuggestionCommand; + } + } + return suggestion; + })); + } + return []; +} +function getSuggestionsForInputValues(token, typeInfo, queryText, schema) { + const namedInputType = (0, _graphql.getNamedType)(typeInfo.inputType); + const queryVariables = getVariableCompletions(queryText, schema, token).filter(v => v.detail === namedInputType.name); + if (namedInputType instanceof _graphql.GraphQLEnumType) { + const values = namedInputType.getValues(); + return (0, _autocompleteUtils.hintList)(token, values.map(value => { + var _a; + return { + label: value.name, + detail: String(namedInputType), + documentation: (_a = value.description) !== null && _a !== void 0 ? _a : undefined, + deprecated: Boolean(value.deprecationReason), + isDeprecated: Boolean(value.deprecationReason), + deprecationReason: value.deprecationReason, + kind: _types.CompletionItemKind.EnumMember, + type: namedInputType + }; + }).concat(queryVariables)); + } + if (namedInputType === _graphql.GraphQLBoolean) { + return (0, _autocompleteUtils.hintList)(token, queryVariables.concat([{ + label: 'true', + detail: String(_graphql.GraphQLBoolean), + documentation: 'Not false.', + kind: _types.CompletionItemKind.Variable, + type: _graphql.GraphQLBoolean + }, { + label: 'false', + detail: String(_graphql.GraphQLBoolean), + documentation: 'Not true.', + kind: _types.CompletionItemKind.Variable, + type: _graphql.GraphQLBoolean + }])); + } + return queryVariables; +} +function getSuggestionsForImplements(token, tokenState, schema, documentText, typeInfo) { + if (tokenState.needsSeparator) { + return []; + } + const typeMap = schema.getTypeMap(); + const schemaInterfaces = (0, _autocompleteUtils.objectValues)(typeMap).filter(_graphql.isInterfaceType); + const schemaInterfaceNames = schemaInterfaces.map(_ref => { + let { + name + } = _ref; + return name; + }); + const inlineInterfaces = new Set(); + runOnlineParser(documentText, (_, state) => { + var _a, _b, _c, _d, _e; + if (state.name) { + if (state.kind === _parser.RuleKinds.INTERFACE_DEF && !schemaInterfaceNames.includes(state.name)) { + inlineInterfaces.add(state.name); + } + if (state.kind === _parser.RuleKinds.NAMED_TYPE && ((_a = state.prevState) === null || _a === void 0 ? void 0 : _a.kind) === _parser.RuleKinds.IMPLEMENTS) { + if (typeInfo.interfaceDef) { + const existingType = (_b = typeInfo.interfaceDef) === null || _b === void 0 ? void 0 : _b.getInterfaces().find(_ref2 => { + let { + name + } = _ref2; + return name === state.name; + }); + if (existingType) { + return; + } + const type = schema.getType(state.name); + const interfaceConfig = (_c = typeInfo.interfaceDef) === null || _c === void 0 ? void 0 : _c.toConfig(); + typeInfo.interfaceDef = new _graphql.GraphQLInterfaceType(Object.assign(Object.assign({}, interfaceConfig), { + interfaces: [...interfaceConfig.interfaces, type || new _graphql.GraphQLInterfaceType({ + name: state.name, + fields: {} + })] + })); + } else if (typeInfo.objectTypeDef) { + const existingType = (_d = typeInfo.objectTypeDef) === null || _d === void 0 ? void 0 : _d.getInterfaces().find(_ref3 => { + let { + name + } = _ref3; + return name === state.name; + }); + if (existingType) { + return; + } + const type = schema.getType(state.name); + const objectTypeConfig = (_e = typeInfo.objectTypeDef) === null || _e === void 0 ? void 0 : _e.toConfig(); + typeInfo.objectTypeDef = new _graphql.GraphQLObjectType(Object.assign(Object.assign({}, objectTypeConfig), { + interfaces: [...objectTypeConfig.interfaces, type || new _graphql.GraphQLInterfaceType({ + name: state.name, + fields: {} + })] + })); + } + } + } + }); + const currentTypeToExtend = typeInfo.interfaceDef || typeInfo.objectTypeDef; + const siblingInterfaces = (currentTypeToExtend === null || currentTypeToExtend === void 0 ? void 0 : currentTypeToExtend.getInterfaces()) || []; + const siblingInterfaceNames = siblingInterfaces.map(_ref4 => { + let { + name + } = _ref4; + return name; + }); + const possibleInterfaces = schemaInterfaces.concat([...inlineInterfaces].map(name => ({ + name + }))).filter(_ref5 => { + let { + name + } = _ref5; + return name !== (currentTypeToExtend === null || currentTypeToExtend === void 0 ? void 0 : currentTypeToExtend.name) && !siblingInterfaceNames.includes(name); + }); + return (0, _autocompleteUtils.hintList)(token, possibleInterfaces.map(type => { + const result = { + label: type.name, + kind: _types.CompletionItemKind.Interface, + type + }; + if (type === null || type === void 0 ? void 0 : type.description) { + result.documentation = type.description; + } + return result; + })); +} +function getSuggestionsForFragmentTypeConditions(token, typeInfo, schema, _kind) { + let possibleTypes; + if (typeInfo.parentType) { + if ((0, _graphql.isAbstractType)(typeInfo.parentType)) { + const abstractType = (0, _graphql.assertAbstractType)(typeInfo.parentType); + const possibleObjTypes = schema.getPossibleTypes(abstractType); + const possibleIfaceMap = Object.create(null); + for (const type of possibleObjTypes) { + for (const iface of type.getInterfaces()) { + possibleIfaceMap[iface.name] = iface; + } + } + possibleTypes = possibleObjTypes.concat((0, _autocompleteUtils.objectValues)(possibleIfaceMap)); + } else { + possibleTypes = [typeInfo.parentType]; + } + } else { + const typeMap = schema.getTypeMap(); + possibleTypes = (0, _autocompleteUtils.objectValues)(typeMap).filter(type => (0, _graphql.isCompositeType)(type) && !type.name.startsWith('__')); + } + return (0, _autocompleteUtils.hintList)(token, possibleTypes.map(type => { + const namedType = (0, _graphql.getNamedType)(type); + return { + label: String(type), + documentation: (namedType === null || namedType === void 0 ? void 0 : namedType.description) || '', + kind: _types.CompletionItemKind.Field + }; + })); +} +function getSuggestionsForFragmentSpread(token, typeInfo, schema, queryText, fragmentDefs) { + if (!queryText) { + return []; + } + const typeMap = schema.getTypeMap(); + const defState = (0, _autocompleteUtils.getDefinitionState)(token.state); + const fragments = getFragmentDefinitions(queryText); + if (fragmentDefs && fragmentDefs.length > 0) { + fragments.push(...fragmentDefs); + } + const relevantFrags = fragments.filter(frag => typeMap[frag.typeCondition.name.value] && !(defState && defState.kind === _parser.RuleKinds.FRAGMENT_DEFINITION && defState.name === frag.name.value) && (0, _graphql.isCompositeType)(typeInfo.parentType) && (0, _graphql.isCompositeType)(typeMap[frag.typeCondition.name.value]) && (0, _graphql.doTypesOverlap)(schema, typeInfo.parentType, typeMap[frag.typeCondition.name.value])); + return (0, _autocompleteUtils.hintList)(token, relevantFrags.map(frag => ({ + label: frag.name.value, + detail: String(typeMap[frag.typeCondition.name.value]), + documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`, + kind: _types.CompletionItemKind.Field, + type: typeMap[frag.typeCondition.name.value] + }))); +} +const getParentDefinition = (state, kind) => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; + if (((_a = state.prevState) === null || _a === void 0 ? void 0 : _a.kind) === kind) { + return state.prevState; + } + if (((_c = (_b = state.prevState) === null || _b === void 0 ? void 0 : _b.prevState) === null || _c === void 0 ? void 0 : _c.kind) === kind) { + return state.prevState.prevState; + } + if (((_f = (_e = (_d = state.prevState) === null || _d === void 0 ? void 0 : _d.prevState) === null || _e === void 0 ? void 0 : _e.prevState) === null || _f === void 0 ? void 0 : _f.kind) === kind) { + return state.prevState.prevState.prevState; + } + if (((_k = (_j = (_h = (_g = state.prevState) === null || _g === void 0 ? void 0 : _g.prevState) === null || _h === void 0 ? void 0 : _h.prevState) === null || _j === void 0 ? void 0 : _j.prevState) === null || _k === void 0 ? void 0 : _k.kind) === kind) { + return state.prevState.prevState.prevState.prevState; + } +}; +function getVariableCompletions(queryText, schema, token) { + let variableName = null; + let variableType; + const definitions = Object.create({}); + runOnlineParser(queryText, (_, state) => { + if ((state === null || state === void 0 ? void 0 : state.kind) === _parser.RuleKinds.VARIABLE && state.name) { + variableName = state.name; + } + if ((state === null || state === void 0 ? void 0 : state.kind) === _parser.RuleKinds.NAMED_TYPE && variableName) { + const parentDefinition = getParentDefinition(state, _parser.RuleKinds.TYPE); + if (parentDefinition === null || parentDefinition === void 0 ? void 0 : parentDefinition.type) { + variableType = schema.getType(parentDefinition === null || parentDefinition === void 0 ? void 0 : parentDefinition.type); + } + } + if (variableName && variableType && !definitions[variableName]) { + definitions[variableName] = { + detail: variableType.toString(), + insertText: token.string === '$' ? variableName : '$' + variableName, + label: variableName, + type: variableType, + kind: _types.CompletionItemKind.Variable + }; + variableName = null; + variableType = null; + } + }); + return (0, _autocompleteUtils.objectValues)(definitions); +} +function getFragmentDefinitions(queryText) { + const fragmentDefs = []; + runOnlineParser(queryText, (_, state) => { + if (state.kind === _parser.RuleKinds.FRAGMENT_DEFINITION && state.name && state.type) { + fragmentDefs.push({ + kind: _parser.RuleKinds.FRAGMENT_DEFINITION, + name: { + kind: _graphql.Kind.NAME, + value: state.name + }, + selectionSet: { + kind: _parser.RuleKinds.SELECTION_SET, + selections: [] + }, + typeCondition: { + kind: _parser.RuleKinds.NAMED_TYPE, + name: { + kind: _graphql.Kind.NAME, + value: state.type + } + } + }); + } + }); + return fragmentDefs; +} +function getSuggestionsForVariableDefinition(token, schema, _kind) { + const inputTypeMap = schema.getTypeMap(); + const inputTypes = (0, _autocompleteUtils.objectValues)(inputTypeMap).filter(_graphql.isInputType); + return (0, _autocompleteUtils.hintList)(token, inputTypes.map(type => ({ + label: type.name, + documentation: type.description, + kind: _types.CompletionItemKind.Variable + }))); +} +function getSuggestionsForDirective(token, state, schema, _kind) { + var _a; + if ((_a = state.prevState) === null || _a === void 0 ? void 0 : _a.kind) { + const directives = schema.getDirectives().filter(directive => canUseDirective(state.prevState, directive)); + return (0, _autocompleteUtils.hintList)(token, directives.map(directive => ({ + label: directive.name, + documentation: directive.description || '', + kind: _types.CompletionItemKind.Function + }))); + } + return []; +} +function getTokenAtPosition(queryText, cursor) { + let offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + let styleAtCursor = null; + let stateAtCursor = null; + let stringAtCursor = null; + const token = runOnlineParser(queryText, (stream, state, style, index) => { + if (index !== cursor.line || stream.getCurrentPosition() + offset < cursor.character + 1) { + return; + } + styleAtCursor = style; + stateAtCursor = Object.assign({}, state); + stringAtCursor = stream.current(); + return 'BREAK'; + }); + return { + start: token.start, + end: token.end, + string: stringAtCursor || token.string, + state: stateAtCursor || token.state, + style: styleAtCursor || token.style + }; +} +function runOnlineParser(queryText, callback) { + const lines = queryText.split('\n'); + const parser = (0, _parser.onlineParser)(); + let state = parser.startState(); + let style = ''; + let stream = new _parser.CharacterStream(''); + for (let i = 0; i < lines.length; i++) { + stream = new _parser.CharacterStream(lines[i]); + while (!stream.eol()) { + style = parser.token(stream, state); + const code = callback(stream, state, style, i); + if (code === 'BREAK') { + break; + } + } + callback(stream, state, style, i); + if (!state.kind) { + state = parser.startState(); + } + } + return { + start: stream.getStartOfToken(), + end: stream.getCurrentPosition(), + string: stream.current(), + state, + style + }; +} +function canUseDirective(state, directive) { + if (!(state === null || state === void 0 ? void 0 : state.kind)) { + return false; + } + const { + kind, + prevState + } = state; + const { + locations + } = directive; + switch (kind) { + case _parser.RuleKinds.QUERY: + return locations.includes(_graphql.DirectiveLocation.QUERY); + case _parser.RuleKinds.MUTATION: + return locations.includes(_graphql.DirectiveLocation.MUTATION); + case _parser.RuleKinds.SUBSCRIPTION: + return locations.includes(_graphql.DirectiveLocation.SUBSCRIPTION); + case _parser.RuleKinds.FIELD: + case _parser.RuleKinds.ALIASED_FIELD: + return locations.includes(_graphql.DirectiveLocation.FIELD); + case _parser.RuleKinds.FRAGMENT_DEFINITION: + return locations.includes(_graphql.DirectiveLocation.FRAGMENT_DEFINITION); + case _parser.RuleKinds.FRAGMENT_SPREAD: + return locations.includes(_graphql.DirectiveLocation.FRAGMENT_SPREAD); + case _parser.RuleKinds.INLINE_FRAGMENT: + return locations.includes(_graphql.DirectiveLocation.INLINE_FRAGMENT); + case _parser.RuleKinds.SCHEMA_DEF: + return locations.includes(_graphql.DirectiveLocation.SCHEMA); + case _parser.RuleKinds.SCALAR_DEF: + return locations.includes(_graphql.DirectiveLocation.SCALAR); + case _parser.RuleKinds.OBJECT_TYPE_DEF: + return locations.includes(_graphql.DirectiveLocation.OBJECT); + case _parser.RuleKinds.FIELD_DEF: + return locations.includes(_graphql.DirectiveLocation.FIELD_DEFINITION); + case _parser.RuleKinds.INTERFACE_DEF: + return locations.includes(_graphql.DirectiveLocation.INTERFACE); + case _parser.RuleKinds.UNION_DEF: + return locations.includes(_graphql.DirectiveLocation.UNION); + case _parser.RuleKinds.ENUM_DEF: + return locations.includes(_graphql.DirectiveLocation.ENUM); + case _parser.RuleKinds.ENUM_VALUE: + return locations.includes(_graphql.DirectiveLocation.ENUM_VALUE); + case _parser.RuleKinds.INPUT_DEF: + return locations.includes(_graphql.DirectiveLocation.INPUT_OBJECT); + case _parser.RuleKinds.INPUT_VALUE_DEF: + const prevStateKind = prevState === null || prevState === void 0 ? void 0 : prevState.kind; + switch (prevStateKind) { + case _parser.RuleKinds.ARGUMENTS_DEF: + return locations.includes(_graphql.DirectiveLocation.ARGUMENT_DEFINITION); + case _parser.RuleKinds.INPUT_DEF: + return locations.includes(_graphql.DirectiveLocation.INPUT_FIELD_DEFINITION); + } + } + return false; +} +function getTypeInfo(schema, tokenState) { + let argDef; + let argDefs; + let directiveDef; + let enumValue; + let fieldDef; + let inputType; + let objectTypeDef; + let objectFieldDefs; + let parentType; + let type; + let interfaceDef; + (0, _autocompleteUtils.forEachState)(tokenState, state => { + var _a; + switch (state.kind) { + case _parser.RuleKinds.QUERY: + case 'ShortQuery': + type = schema.getQueryType(); + break; + case _parser.RuleKinds.MUTATION: + type = schema.getMutationType(); + break; + case _parser.RuleKinds.SUBSCRIPTION: + type = schema.getSubscriptionType(); + break; + case _parser.RuleKinds.INLINE_FRAGMENT: + case _parser.RuleKinds.FRAGMENT_DEFINITION: + if (state.type) { + type = schema.getType(state.type); + } + break; + case _parser.RuleKinds.FIELD: + case _parser.RuleKinds.ALIASED_FIELD: + { + if (!type || !state.name) { + fieldDef = null; + } else { + fieldDef = parentType ? (0, _autocompleteUtils.getFieldDef)(schema, parentType, state.name) : null; + type = fieldDef ? fieldDef.type : null; + } + break; + } + case _parser.RuleKinds.SELECTION_SET: + parentType = (0, _graphql.getNamedType)(type); + break; + case _parser.RuleKinds.DIRECTIVE: + directiveDef = state.name ? schema.getDirective(state.name) : null; + break; + case _parser.RuleKinds.INTERFACE_DEF: + if (state.name) { + objectTypeDef = null; + interfaceDef = new _graphql.GraphQLInterfaceType({ + name: state.name, + interfaces: [], + fields: {} + }); + } + break; + case _parser.RuleKinds.OBJECT_TYPE_DEF: + if (state.name) { + interfaceDef = null; + objectTypeDef = new _graphql.GraphQLObjectType({ + name: state.name, + interfaces: [], + fields: {} + }); + } + break; + case _parser.RuleKinds.ARGUMENTS: + { + if (state.prevState) { + switch (state.prevState.kind) { + case _parser.RuleKinds.FIELD: + argDefs = fieldDef && fieldDef.args; + break; + case _parser.RuleKinds.DIRECTIVE: + argDefs = directiveDef && directiveDef.args; + break; + case _parser.RuleKinds.ALIASED_FIELD: + { + const name = (_a = state.prevState) === null || _a === void 0 ? void 0 : _a.name; + if (!name) { + argDefs = null; + break; + } + const field = parentType ? (0, _autocompleteUtils.getFieldDef)(schema, parentType, name) : null; + if (!field) { + argDefs = null; + break; + } + argDefs = field.args; + break; + } + default: + argDefs = null; + break; + } + } else { + argDefs = null; + } + break; + } + case _parser.RuleKinds.ARGUMENT: + if (argDefs) { + for (let i = 0; i < argDefs.length; i++) { + if (argDefs[i].name === state.name) { + argDef = argDefs[i]; + break; + } + } + } + inputType = argDef === null || argDef === void 0 ? void 0 : argDef.type; + break; + case _parser.RuleKinds.ENUM_VALUE: + const enumType = (0, _graphql.getNamedType)(inputType); + enumValue = enumType instanceof _graphql.GraphQLEnumType ? enumType.getValues().find(val => val.value === state.name) : null; + break; + case _parser.RuleKinds.LIST_VALUE: + const nullableType = (0, _graphql.getNullableType)(inputType); + inputType = nullableType instanceof _graphql.GraphQLList ? nullableType.ofType : null; + break; + case _parser.RuleKinds.OBJECT_VALUE: + const objectType = (0, _graphql.getNamedType)(inputType); + objectFieldDefs = objectType instanceof _graphql.GraphQLInputObjectType ? objectType.getFields() : null; + break; + case _parser.RuleKinds.OBJECT_FIELD: + const objectField = state.name && objectFieldDefs ? objectFieldDefs[state.name] : null; + inputType = objectField === null || objectField === void 0 ? void 0 : objectField.type; + break; + case _parser.RuleKinds.NAMED_TYPE: + if (state.name) { + type = schema.getType(state.name); + } + break; + } + }); + return { + argDef, + argDefs, + directiveDef, + enumValue, + fieldDef, + inputType, + objectFieldDefs, + parentType, + type, + interfaceDef, + objectTypeDef + }; +} +var GraphQLDocumentMode; +exports.GraphQLDocumentMode = GraphQLDocumentMode; +(function (GraphQLDocumentMode) { + GraphQLDocumentMode["TYPE_SYSTEM"] = "TYPE_SYSTEM"; + GraphQLDocumentMode["EXECUTABLE"] = "EXECUTABLE"; +})(GraphQLDocumentMode || (exports.GraphQLDocumentMode = GraphQLDocumentMode = {})); +function getDocumentMode(documentText, uri) { + if (uri === null || uri === void 0 ? void 0 : uri.endsWith('.graphqls')) { + return GraphQLDocumentMode.TYPE_SYSTEM; + } + return hasTypeSystemDefinitions(documentText) ? GraphQLDocumentMode.TYPE_SYSTEM : GraphQLDocumentMode.EXECUTABLE; +} +function unwrapType(state) { + if (state.prevState && state.kind && [_parser.RuleKinds.NAMED_TYPE, _parser.RuleKinds.LIST_TYPE, _parser.RuleKinds.TYPE, _parser.RuleKinds.NON_NULL_TYPE].includes(state.kind)) { + return unwrapType(state.prevState); + } + return state; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/getDefinition.js": +/*!*********************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/getDefinition.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.LANGUAGE = void 0; +exports.getDefinitionQueryResultForDefinitionNode = getDefinitionQueryResultForDefinitionNode; +exports.getDefinitionQueryResultForField = getDefinitionQueryResultForField; +exports.getDefinitionQueryResultForFragmentSpread = getDefinitionQueryResultForFragmentSpread; +exports.getDefinitionQueryResultForNamedType = getDefinitionQueryResultForNamedType; +var _utils = __webpack_require__(/*! ../utils */ "../../graphql-language-service/esm/utils/index.js"); +var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P ? value : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +const LANGUAGE = 'GraphQL'; +exports.LANGUAGE = LANGUAGE; +function assert(value, message) { + if (!value) { + throw new Error(message); + } +} +function getRange(text, node) { + const location = node.loc; + assert(location, 'Expected ASTNode to have a location.'); + return (0, _utils.locToRange)(text, location); +} +function getPosition(text, node) { + const location = node.loc; + assert(location, 'Expected ASTNode to have a location.'); + return (0, _utils.offsetToPosition)(text, location.start); +} +function getDefinitionQueryResultForNamedType(text, node, dependencies) { + return __awaiter(this, void 0, void 0, function* () { + const name = node.name.value; + const defNodes = dependencies.filter(_ref => { + let { + definition + } = _ref; + return definition.name && definition.name.value === name; + }); + if (defNodes.length === 0) { + throw new Error(`Definition not found for GraphQL type ${name}`); + } + const definitions = defNodes.map(_ref2 => { + let { + filePath, + content, + definition + } = _ref2; + return getDefinitionForNodeDefinition(filePath || '', content, definition); + }); + return { + definitions, + queryRange: definitions.map(_ => getRange(text, node)) + }; + }); +} +function getDefinitionQueryResultForField(fieldName, typeName, dependencies) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const defNodes = dependencies.filter(_ref3 => { + let { + definition + } = _ref3; + return definition.name && definition.name.value === typeName; + }); + if (defNodes.length === 0) { + throw new Error(`Definition not found for GraphQL type ${typeName}`); + } + const definitions = []; + for (const { + filePath, + content, + definition + } of defNodes) { + const fieldDefinition = (_a = definition.fields) === null || _a === void 0 ? void 0 : _a.find(item => item.name.value === fieldName); + if (fieldDefinition == null) { + continue; + } + definitions.push(getDefinitionForFieldDefinition(filePath || '', content, fieldDefinition)); + } + return { + definitions, + queryRange: [] + }; + }); +} +function getDefinitionQueryResultForFragmentSpread(text, fragment, dependencies) { + return __awaiter(this, void 0, void 0, function* () { + const name = fragment.name.value; + const defNodes = dependencies.filter(_ref4 => { + let { + definition + } = _ref4; + return definition.name.value === name; + }); + if (defNodes.length === 0) { + throw new Error(`Definition not found for GraphQL fragment ${name}`); + } + const definitions = defNodes.map(_ref5 => { + let { + filePath, + content, + definition + } = _ref5; + return getDefinitionForFragmentDefinition(filePath || '', content, definition); + }); + return { + definitions, + queryRange: definitions.map(_ => getRange(text, fragment)) + }; + }); +} +function getDefinitionQueryResultForDefinitionNode(path, text, definition) { + return { + definitions: [getDefinitionForFragmentDefinition(path, text, definition)], + queryRange: definition.name ? [getRange(text, definition.name)] : [] + }; +} +function getDefinitionForFragmentDefinition(path, text, definition) { + const { + name + } = definition; + if (!name) { + throw new Error('Expected ASTNode to have a Name.'); + } + return { + path, + position: getPosition(text, definition), + range: getRange(text, definition), + name: name.value || '', + language: LANGUAGE, + projectRoot: path + }; +} +function getDefinitionForNodeDefinition(path, text, definition) { + const { + name + } = definition; + assert(name, 'Expected ASTNode to have a Name.'); + return { + path, + position: getPosition(text, definition), + range: getRange(text, definition), + name: name.value || '', + language: LANGUAGE, + projectRoot: path + }; +} +function getDefinitionForFieldDefinition(path, text, definition) { + const { + name + } = definition; + assert(name, 'Expected ASTNode to have a Name.'); + return { + path, + position: getPosition(text, definition), + range: getRange(text, definition), + name: name.value || '', + language: LANGUAGE, + projectRoot: path + }; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/getDiagnostics.js": +/*!**********************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/getDiagnostics.js ***! + \**********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SEVERITY = exports.DIAGNOSTIC_SEVERITY = void 0; +exports.getDiagnostics = getDiagnostics; +exports.getRange = getRange; +exports.validateQuery = validateQuery; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _parser = __webpack_require__(/*! ../parser */ "../../graphql-language-service/esm/parser/index.js"); +var _utils = __webpack_require__(/*! ../utils */ "../../graphql-language-service/esm/utils/index.js"); +const SEVERITY = { + Error: 'Error', + Warning: 'Warning', + Information: 'Information', + Hint: 'Hint' +}; +exports.SEVERITY = SEVERITY; +const DIAGNOSTIC_SEVERITY = { + [SEVERITY.Error]: 1, + [SEVERITY.Warning]: 2, + [SEVERITY.Information]: 3, + [SEVERITY.Hint]: 4 +}; +exports.DIAGNOSTIC_SEVERITY = DIAGNOSTIC_SEVERITY; +const invariant = (condition, message) => { + if (!condition) { + throw new Error(message); + } +}; +function getDiagnostics(query) { + let schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + let customRules = arguments.length > 2 ? arguments[2] : undefined; + let isRelayCompatMode = arguments.length > 3 ? arguments[3] : undefined; + let externalFragments = arguments.length > 4 ? arguments[4] : undefined; + var _a, _b; + let ast = null; + let fragments = ''; + if (externalFragments) { + fragments = typeof externalFragments === 'string' ? externalFragments : externalFragments.reduce((acc, node) => acc + (0, _graphql.print)(node) + '\n\n', ''); + } + const enhancedQuery = fragments ? `${query}\n\n${fragments}` : query; + try { + ast = (0, _graphql.parse)(enhancedQuery); + } catch (error) { + if (error instanceof _graphql.GraphQLError) { + const range = getRange((_b = (_a = error.locations) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : { + line: 0, + column: 0 + }, enhancedQuery); + return [{ + severity: DIAGNOSTIC_SEVERITY.Error, + message: error.message, + source: 'GraphQL: Syntax', + range + }]; + } + throw error; + } + return validateQuery(ast, schema, customRules, isRelayCompatMode); +} +function validateQuery(ast) { + let schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + let customRules = arguments.length > 2 ? arguments[2] : undefined; + let isRelayCompatMode = arguments.length > 3 ? arguments[3] : undefined; + if (!schema) { + return []; + } + const validationErrorAnnotations = (0, _utils.validateWithCustomRules)(schema, ast, customRules, isRelayCompatMode).flatMap(error => annotations(error, DIAGNOSTIC_SEVERITY.Error, 'Validation')); + const deprecationWarningAnnotations = (0, _graphql.validate)(schema, ast, [_graphql.NoDeprecatedCustomRule]).flatMap(error => annotations(error, DIAGNOSTIC_SEVERITY.Warning, 'Deprecation')); + return validationErrorAnnotations.concat(deprecationWarningAnnotations); +} +function annotations(error, severity, type) { + if (!error.nodes) { + return []; + } + const highlightedNodes = []; + for (const [i, node] of error.nodes.entries()) { + const highlightNode = node.kind !== 'Variable' && 'name' in node && node.name !== undefined ? node.name : 'variable' in node && node.variable !== undefined ? node.variable : node; + if (highlightNode) { + invariant(error.locations, 'GraphQL validation error requires locations.'); + const loc = error.locations[i]; + const highlightLoc = getLocation(highlightNode); + const end = loc.column + (highlightLoc.end - highlightLoc.start); + highlightedNodes.push({ + source: `GraphQL: ${type}`, + message: error.message, + severity, + range: new _utils.Range(new _utils.Position(loc.line - 1, loc.column - 1), new _utils.Position(loc.line - 1, end)) + }); + } + } + return highlightedNodes; +} +function getRange(location, queryText) { + const parser = (0, _parser.onlineParser)(); + const state = parser.startState(); + const lines = queryText.split('\n'); + invariant(lines.length >= location.line, 'Query text must have more lines than where the error happened'); + let stream = null; + for (let i = 0; i < location.line; i++) { + stream = new _parser.CharacterStream(lines[i]); + while (!stream.eol()) { + const style = parser.token(stream, state); + if (style === 'invalidchar') { + break; + } + } + } + invariant(stream, 'Expected Parser stream to be available.'); + const line = location.line - 1; + const start = stream.getStartOfToken(); + const end = stream.getCurrentPosition(); + return new _utils.Range(new _utils.Position(line, start), new _utils.Position(line, end)); +} +function getLocation(node) { + const typeCastedNode = node; + const location = typeCastedNode.loc; + invariant(location, 'Expected ASTNode to have a location.'); + return location; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/getHoverInformation.js": +/*!***************************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/getHoverInformation.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getHoverInformation = getHoverInformation; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _getAutocompleteSuggestions = __webpack_require__(/*! ./getAutocompleteSuggestions */ "../../graphql-language-service/esm/interface/getAutocompleteSuggestions.js"); +function getHoverInformation(schema, queryText, cursor, contextToken, config) { + const token = contextToken || (0, _getAutocompleteSuggestions.getTokenAtPosition)(queryText, cursor); + if (!schema || !token || !token.state) { + return ''; + } + const { + kind, + step + } = token.state; + const typeInfo = (0, _getAutocompleteSuggestions.getTypeInfo)(schema, token.state); + const options = Object.assign(Object.assign({}, config), { + schema + }); + if (kind === 'Field' && step === 0 && typeInfo.fieldDef || kind === 'AliasedField' && step === 2 && typeInfo.fieldDef) { + const into = []; + renderMdCodeStart(into, options); + renderField(into, typeInfo, options); + renderMdCodeEnd(into, options); + renderDescription(into, options, typeInfo.fieldDef); + return into.join('').trim(); + } + if (kind === 'Directive' && step === 1 && typeInfo.directiveDef) { + const into = []; + renderMdCodeStart(into, options); + renderDirective(into, typeInfo, options); + renderMdCodeEnd(into, options); + renderDescription(into, options, typeInfo.directiveDef); + return into.join('').trim(); + } + if (kind === 'Argument' && step === 0 && typeInfo.argDef) { + const into = []; + renderMdCodeStart(into, options); + renderArg(into, typeInfo, options); + renderMdCodeEnd(into, options); + renderDescription(into, options, typeInfo.argDef); + return into.join('').trim(); + } + if (kind === 'EnumValue' && typeInfo.enumValue && 'description' in typeInfo.enumValue) { + const into = []; + renderMdCodeStart(into, options); + renderEnumValue(into, typeInfo, options); + renderMdCodeEnd(into, options); + renderDescription(into, options, typeInfo.enumValue); + return into.join('').trim(); + } + if (kind === 'NamedType' && typeInfo.type && 'description' in typeInfo.type) { + const into = []; + renderMdCodeStart(into, options); + renderType(into, typeInfo, options, typeInfo.type); + renderMdCodeEnd(into, options); + renderDescription(into, options, typeInfo.type); + return into.join('').trim(); + } + return ''; +} +function renderMdCodeStart(into, options) { + if (options.useMarkdown) { + text(into, '```graphql\n'); + } +} +function renderMdCodeEnd(into, options) { + if (options.useMarkdown) { + text(into, '\n```'); + } +} +function renderField(into, typeInfo, options) { + renderQualifiedField(into, typeInfo, options); + renderTypeAnnotation(into, typeInfo, options, typeInfo.type); +} +function renderQualifiedField(into, typeInfo, options) { + if (!typeInfo.fieldDef) { + return; + } + const fieldName = typeInfo.fieldDef.name; + if (fieldName.slice(0, 2) !== '__') { + renderType(into, typeInfo, options, typeInfo.parentType); + text(into, '.'); + } + text(into, fieldName); +} +function renderDirective(into, typeInfo, _options) { + if (!typeInfo.directiveDef) { + return; + } + const name = '@' + typeInfo.directiveDef.name; + text(into, name); +} +function renderArg(into, typeInfo, options) { + if (typeInfo.directiveDef) { + renderDirective(into, typeInfo, options); + } else if (typeInfo.fieldDef) { + renderQualifiedField(into, typeInfo, options); + } + if (!typeInfo.argDef) { + return; + } + const { + name + } = typeInfo.argDef; + text(into, '('); + text(into, name); + renderTypeAnnotation(into, typeInfo, options, typeInfo.inputType); + text(into, ')'); +} +function renderTypeAnnotation(into, typeInfo, options, t) { + text(into, ': '); + renderType(into, typeInfo, options, t); +} +function renderEnumValue(into, typeInfo, options) { + if (!typeInfo.enumValue) { + return; + } + const { + name + } = typeInfo.enumValue; + renderType(into, typeInfo, options, typeInfo.inputType); + text(into, '.'); + text(into, name); +} +function renderType(into, typeInfo, options, t) { + if (!t) { + return; + } + if (t instanceof _graphql.GraphQLNonNull) { + renderType(into, typeInfo, options, t.ofType); + text(into, '!'); + } else if (t instanceof _graphql.GraphQLList) { + text(into, '['); + renderType(into, typeInfo, options, t.ofType); + text(into, ']'); + } else { + text(into, t.name); + } +} +function renderDescription(into, options, def) { + if (!def) { + return; + } + const description = typeof def.description === 'string' ? def.description : null; + if (description) { + text(into, '\n\n'); + text(into, description); + } + renderDeprecation(into, options, def); +} +function renderDeprecation(into, _options, def) { + if (!def) { + return; + } + const reason = def.deprecationReason || null; + if (!reason) { + return; + } + text(into, '\n\n'); + text(into, 'Deprecated: '); + text(into, reason); +} +function text(into, content) { + into.push(content); +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/getOutline.js": +/*!******************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/getOutline.js ***! + \******************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getOutline = getOutline; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _utils = __webpack_require__(/*! ../utils */ "../../graphql-language-service/esm/utils/index.js"); +const { + INLINE_FRAGMENT +} = _graphql.Kind; +const OUTLINEABLE_KINDS = { + Field: true, + OperationDefinition: true, + Document: true, + SelectionSet: true, + Name: true, + FragmentDefinition: true, + FragmentSpread: true, + InlineFragment: true, + ObjectTypeDefinition: true, + InputObjectTypeDefinition: true, + InterfaceTypeDefinition: true, + EnumTypeDefinition: true, + EnumValueDefinition: true, + InputValueDefinition: true, + FieldDefinition: true +}; +function getOutline(documentText) { + let ast; + try { + ast = (0, _graphql.parse)(documentText); + } catch (_a) { + return null; + } + const visitorFns = outlineTreeConverter(documentText); + const outlineTrees = (0, _graphql.visit)(ast, { + leave(node) { + if (visitorFns !== undefined && node.kind in visitorFns) { + return visitorFns[node.kind](node); + } + return null; + } + }); + return { + outlineTrees + }; +} +function outlineTreeConverter(docText) { + const meta = node => { + return { + representativeName: node.name, + startPosition: (0, _utils.offsetToPosition)(docText, node.loc.start), + endPosition: (0, _utils.offsetToPosition)(docText, node.loc.end), + kind: node.kind, + children: node.selectionSet || node.fields || node.values || node.arguments || [] + }; + }; + return { + Field(node) { + const tokenizedText = node.alias ? [buildToken('plain', node.alias), buildToken('plain', ': ')] : []; + tokenizedText.push(buildToken('plain', node.name)); + return Object.assign({ + tokenizedText + }, meta(node)); + }, + OperationDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', node.operation), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + Document: node => node.definitions, + SelectionSet: node => concatMap(node.selections, child => { + return child.kind === INLINE_FRAGMENT ? child.selectionSet : child; + }), + Name: node => node.value, + FragmentDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', 'fragment'), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + InterfaceTypeDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', 'interface'), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + EnumTypeDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', 'enum'), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + EnumValueDefinition: node => Object.assign({ + tokenizedText: [buildToken('plain', node.name)] + }, meta(node)), + ObjectTypeDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', 'type'), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + InputObjectTypeDefinition: node => Object.assign({ + tokenizedText: [buildToken('keyword', 'input'), buildToken('whitespace', ' '), buildToken('class-name', node.name)] + }, meta(node)), + FragmentSpread: node => Object.assign({ + tokenizedText: [buildToken('plain', '...'), buildToken('class-name', node.name)] + }, meta(node)), + InputValueDefinition(node) { + return Object.assign({ + tokenizedText: [buildToken('plain', node.name)] + }, meta(node)); + }, + FieldDefinition(node) { + return Object.assign({ + tokenizedText: [buildToken('plain', node.name)] + }, meta(node)); + }, + InlineFragment: node => node.selectionSet + }; +} +function buildToken(kind, value) { + return { + kind, + value + }; +} +function concatMap(arr, fn) { + const res = []; + for (let i = 0; i < arr.length; i++) { + const x = fn(arr[i], i); + if (Array.isArray(x)) { + res.push(...x); + } else { + res.push(x); + } + } + return res; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/interface/index.js": +/*!*************************************************************!*\ + !*** ../../graphql-language-service/esm/interface/index.js ***! + \*************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _exportNames = { + getOutline: true, + getHoverInformation: true +}; +Object.defineProperty(exports, "getHoverInformation", ({ + enumerable: true, + get: function () { + return _getHoverInformation.getHoverInformation; + } +})); +Object.defineProperty(exports, "getOutline", ({ + enumerable: true, + get: function () { + return _getOutline.getOutline; + } +})); +var _autocompleteUtils = __webpack_require__(/*! ./autocompleteUtils */ "../../graphql-language-service/esm/interface/autocompleteUtils.js"); +Object.keys(_autocompleteUtils).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _autocompleteUtils[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _autocompleteUtils[key]; + } + }); +}); +var _getAutocompleteSuggestions = __webpack_require__(/*! ./getAutocompleteSuggestions */ "../../graphql-language-service/esm/interface/getAutocompleteSuggestions.js"); +Object.keys(_getAutocompleteSuggestions).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _getAutocompleteSuggestions[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _getAutocompleteSuggestions[key]; + } + }); +}); +var _getDefinition = __webpack_require__(/*! ./getDefinition */ "../../graphql-language-service/esm/interface/getDefinition.js"); +Object.keys(_getDefinition).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _getDefinition[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _getDefinition[key]; + } + }); +}); +var _getDiagnostics = __webpack_require__(/*! ./getDiagnostics */ "../../graphql-language-service/esm/interface/getDiagnostics.js"); +Object.keys(_getDiagnostics).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _getDiagnostics[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _getDiagnostics[key]; + } + }); +}); +var _getOutline = __webpack_require__(/*! ./getOutline */ "../../graphql-language-service/esm/interface/getOutline.js"); +var _getHoverInformation = __webpack_require__(/*! ./getHoverInformation */ "../../graphql-language-service/esm/interface/getHoverInformation.js"); + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/CharacterStream.js": +/*!********************************************************************!*\ + !*** ../../graphql-language-service/esm/parser/CharacterStream.js ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +class CharacterStream { + constructor(sourceText) { + var _this = this; + this._start = 0; + this._pos = 0; + this.getStartOfToken = () => this._start; + this.getCurrentPosition = () => this._pos; + this.eol = () => this._sourceText.length === this._pos; + this.sol = () => this._pos === 0; + this.peek = () => { + return this._sourceText.charAt(this._pos) || null; + }; + this.next = () => { + const char = this._sourceText.charAt(this._pos); + this._pos++; + return char; + }; + this.eat = pattern => { + const isMatched = this._testNextCharacter(pattern); + if (isMatched) { + this._start = this._pos; + this._pos++; + return this._sourceText.charAt(this._pos - 1); + } + return undefined; + }; + this.eatWhile = match => { + let isMatched = this._testNextCharacter(match); + let didEat = false; + if (isMatched) { + didEat = isMatched; + this._start = this._pos; + } + while (isMatched) { + this._pos++; + isMatched = this._testNextCharacter(match); + didEat = true; + } + return didEat; + }; + this.eatSpace = () => this.eatWhile(/[\s\u00a0]/); + this.skipToEnd = () => { + this._pos = this._sourceText.length; + }; + this.skipTo = position => { + this._pos = position; + }; + this.match = function (pattern) { + let consume = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + let caseFold = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + let token = null; + let match = null; + if (typeof pattern === 'string') { + const regex = new RegExp(pattern, caseFold ? 'i' : 'g'); + match = regex.test(_this._sourceText.slice(_this._pos, _this._pos + pattern.length)); + token = pattern; + } else if (pattern instanceof RegExp) { + match = _this._sourceText.slice(_this._pos).match(pattern); + token = match === null || match === void 0 ? void 0 : match[0]; + } + if (match != null && (typeof pattern === 'string' || match instanceof Array && _this._sourceText.startsWith(match[0], _this._pos))) { + if (consume) { + _this._start = _this._pos; + if (token && token.length) { + _this._pos += token.length; + } + } + return match; + } + return false; + }; + this.backUp = num => { + this._pos -= num; + }; + this.column = () => this._pos; + this.indentation = () => { + const match = this._sourceText.match(/\s*/); + let indent = 0; + if (match && match.length !== 0) { + const whiteSpaces = match[0]; + let pos = 0; + while (whiteSpaces.length > pos) { + if (whiteSpaces.charCodeAt(pos) === 9) { + indent += 2; + } else { + indent++; + } + pos++; + } + } + return indent; + }; + this.current = () => this._sourceText.slice(this._start, this._pos); + this._sourceText = sourceText; + } + _testNextCharacter(pattern) { + const character = this._sourceText.charAt(this._pos); + let isMatched = false; + if (typeof pattern === 'string') { + isMatched = character === pattern; + } else { + isMatched = pattern instanceof RegExp ? pattern.test(character) : pattern(character); + } + return isMatched; + } +} +exports["default"] = CharacterStream; + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/RuleHelpers.js": +/*!****************************************************************!*\ + !*** ../../graphql-language-service/esm/parser/RuleHelpers.js ***! + \****************************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.butNot = butNot; +exports.list = list; +exports.opt = opt; +exports.p = p; +exports.t = t; +function opt(ofRule) { + return { + ofRule + }; +} +function list(ofRule, separator) { + return { + ofRule, + isList: true, + separator + }; +} +function butNot(rule, exclusions) { + const ruleMatch = rule.match; + rule.match = token => { + let check = false; + if (ruleMatch) { + check = ruleMatch(token); + } + return check && exclusions.every(exclusion => exclusion.match && !exclusion.match(token)); + }; + return rule; +} +function t(kind, style) { + return { + style, + match: token => token.kind === kind + }; +} +function p(value, style) { + return { + style: style || 'punctuation', + match: token => token.kind === 'Punctuation' && token.value === value + }; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/Rules.js": +/*!**********************************************************!*\ + !*** ../../graphql-language-service/esm/parser/Rules.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isIgnored = exports.ParseRules = exports.LexRules = void 0; +var _RuleHelpers = __webpack_require__(/*! ./RuleHelpers */ "../../graphql-language-service/esm/parser/RuleHelpers.js"); +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +const isIgnored = ch => ch === ' ' || ch === '\t' || ch === ',' || ch === '\n' || ch === '\r' || ch === '\uFEFF' || ch === '\u00A0'; +exports.isIgnored = isIgnored; +const LexRules = { + Name: /^[_A-Za-z][_0-9A-Za-z]*/, + Punctuation: /^(?:!|\$|\(|\)|\.\.\.|:|=|&|@|\[|]|\{|\||\})/, + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + String: /^(?:"""(?:\\"""|[^"]|"[^"]|""[^"])*(?:""")?|"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?)/, + Comment: /^#.*/ +}; +exports.LexRules = LexRules; +const ParseRules = { + Document: [(0, _RuleHelpers.list)('Definition')], + Definition(token) { + switch (token.value) { + case '{': + return 'ShortQuery'; + case 'query': + return 'Query'; + case 'mutation': + return 'Mutation'; + case 'subscription': + return 'Subscription'; + case 'fragment': + return _graphql.Kind.FRAGMENT_DEFINITION; + case 'schema': + return 'SchemaDef'; + case 'scalar': + return 'ScalarDef'; + case 'type': + return 'ObjectTypeDef'; + case 'interface': + return 'InterfaceDef'; + case 'union': + return 'UnionDef'; + case 'enum': + return 'EnumDef'; + case 'input': + return 'InputDef'; + case 'extend': + return 'ExtendDef'; + case 'directive': + return 'DirectiveDef'; + } + }, + ShortQuery: ['SelectionSet'], + Query: [word('query'), (0, _RuleHelpers.opt)(name('def')), (0, _RuleHelpers.opt)('VariableDefinitions'), (0, _RuleHelpers.list)('Directive'), 'SelectionSet'], + Mutation: [word('mutation'), (0, _RuleHelpers.opt)(name('def')), (0, _RuleHelpers.opt)('VariableDefinitions'), (0, _RuleHelpers.list)('Directive'), 'SelectionSet'], + Subscription: [word('subscription'), (0, _RuleHelpers.opt)(name('def')), (0, _RuleHelpers.opt)('VariableDefinitions'), (0, _RuleHelpers.list)('Directive'), 'SelectionSet'], + VariableDefinitions: [(0, _RuleHelpers.p)('('), (0, _RuleHelpers.list)('VariableDefinition'), (0, _RuleHelpers.p)(')')], + VariableDefinition: ['Variable', (0, _RuleHelpers.p)(':'), 'Type', (0, _RuleHelpers.opt)('DefaultValue')], + Variable: [(0, _RuleHelpers.p)('$', 'variable'), name('variable')], + DefaultValue: [(0, _RuleHelpers.p)('='), 'Value'], + SelectionSet: [(0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('Selection'), (0, _RuleHelpers.p)('}')], + Selection(token, stream) { + return token.value === '...' ? stream.match(/[\s\u00a0,]*(on\b|@|{)/, false) ? 'InlineFragment' : 'FragmentSpread' : stream.match(/[\s\u00a0,]*:/, false) ? 'AliasedField' : 'Field'; + }, + AliasedField: [name('property'), (0, _RuleHelpers.p)(':'), name('qualifier'), (0, _RuleHelpers.opt)('Arguments'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.opt)('SelectionSet')], + Field: [name('property'), (0, _RuleHelpers.opt)('Arguments'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.opt)('SelectionSet')], + Arguments: [(0, _RuleHelpers.p)('('), (0, _RuleHelpers.list)('Argument'), (0, _RuleHelpers.p)(')')], + Argument: [name('attribute'), (0, _RuleHelpers.p)(':'), 'Value'], + FragmentSpread: [(0, _RuleHelpers.p)('...'), name('def'), (0, _RuleHelpers.list)('Directive')], + InlineFragment: [(0, _RuleHelpers.p)('...'), (0, _RuleHelpers.opt)('TypeCondition'), (0, _RuleHelpers.list)('Directive'), 'SelectionSet'], + FragmentDefinition: [word('fragment'), (0, _RuleHelpers.opt)((0, _RuleHelpers.butNot)(name('def'), [word('on')])), 'TypeCondition', (0, _RuleHelpers.list)('Directive'), 'SelectionSet'], + TypeCondition: [word('on'), 'NamedType'], + Value(token) { + switch (token.kind) { + case 'Number': + return 'NumberValue'; + case 'String': + return 'StringValue'; + case 'Punctuation': + switch (token.value) { + case '[': + return 'ListValue'; + case '{': + return 'ObjectValue'; + case '$': + return 'Variable'; + case '&': + return 'NamedType'; + } + return null; + case 'Name': + switch (token.value) { + case 'true': + case 'false': + return 'BooleanValue'; + } + if (token.value === 'null') { + return 'NullValue'; + } + return 'EnumValue'; + } + }, + NumberValue: [(0, _RuleHelpers.t)('Number', 'number')], + StringValue: [{ + style: 'string', + match: token => token.kind === 'String', + update(state, token) { + if (token.value.startsWith('"""')) { + state.inBlockstring = !token.value.slice(3).endsWith('"""'); + } + } + }], + BooleanValue: [(0, _RuleHelpers.t)('Name', 'builtin')], + NullValue: [(0, _RuleHelpers.t)('Name', 'keyword')], + EnumValue: [name('string-2')], + ListValue: [(0, _RuleHelpers.p)('['), (0, _RuleHelpers.list)('Value'), (0, _RuleHelpers.p)(']')], + ObjectValue: [(0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('ObjectField'), (0, _RuleHelpers.p)('}')], + ObjectField: [name('attribute'), (0, _RuleHelpers.p)(':'), 'Value'], + Type(token) { + return token.value === '[' ? 'ListType' : 'NonNullType'; + }, + ListType: [(0, _RuleHelpers.p)('['), 'Type', (0, _RuleHelpers.p)(']'), (0, _RuleHelpers.opt)((0, _RuleHelpers.p)('!'))], + NonNullType: ['NamedType', (0, _RuleHelpers.opt)((0, _RuleHelpers.p)('!'))], + NamedType: [type('atom')], + Directive: [(0, _RuleHelpers.p)('@', 'meta'), name('meta'), (0, _RuleHelpers.opt)('Arguments')], + DirectiveDef: [word('directive'), (0, _RuleHelpers.p)('@', 'meta'), name('meta'), (0, _RuleHelpers.opt)('ArgumentsDef'), word('on'), (0, _RuleHelpers.list)('DirectiveLocation', (0, _RuleHelpers.p)('|'))], + InterfaceDef: [word('interface'), name('atom'), (0, _RuleHelpers.opt)('Implements'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('FieldDef'), (0, _RuleHelpers.p)('}')], + Implements: [word('implements'), (0, _RuleHelpers.list)('NamedType', (0, _RuleHelpers.p)('&'))], + DirectiveLocation: [name('string-2')], + SchemaDef: [word('schema'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('OperationTypeDef'), (0, _RuleHelpers.p)('}')], + OperationTypeDef: [name('keyword'), (0, _RuleHelpers.p)(':'), name('atom')], + ScalarDef: [word('scalar'), name('atom'), (0, _RuleHelpers.list)('Directive')], + ObjectTypeDef: [word('type'), name('atom'), (0, _RuleHelpers.opt)('Implements'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('FieldDef'), (0, _RuleHelpers.p)('}')], + FieldDef: [name('property'), (0, _RuleHelpers.opt)('ArgumentsDef'), (0, _RuleHelpers.p)(':'), 'Type', (0, _RuleHelpers.list)('Directive')], + ArgumentsDef: [(0, _RuleHelpers.p)('('), (0, _RuleHelpers.list)('InputValueDef'), (0, _RuleHelpers.p)(')')], + InputValueDef: [name('attribute'), (0, _RuleHelpers.p)(':'), 'Type', (0, _RuleHelpers.opt)('DefaultValue'), (0, _RuleHelpers.list)('Directive')], + UnionDef: [word('union'), name('atom'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('='), (0, _RuleHelpers.list)('UnionMember', (0, _RuleHelpers.p)('|'))], + UnionMember: ['NamedType'], + EnumDef: [word('enum'), name('atom'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('EnumValueDef'), (0, _RuleHelpers.p)('}')], + EnumValueDef: [name('string-2'), (0, _RuleHelpers.list)('Directive')], + InputDef: [word('input'), name('atom'), (0, _RuleHelpers.list)('Directive'), (0, _RuleHelpers.p)('{'), (0, _RuleHelpers.list)('InputValueDef'), (0, _RuleHelpers.p)('}')], + ExtendDef: [word('extend'), 'ExtensionDefinition'], + ExtensionDefinition(token) { + switch (token.value) { + case 'schema': + return _graphql.Kind.SCHEMA_EXTENSION; + case 'scalar': + return _graphql.Kind.SCALAR_TYPE_EXTENSION; + case 'type': + return _graphql.Kind.OBJECT_TYPE_EXTENSION; + case 'interface': + return _graphql.Kind.INTERFACE_TYPE_EXTENSION; + case 'union': + return _graphql.Kind.UNION_TYPE_EXTENSION; + case 'enum': + return _graphql.Kind.ENUM_TYPE_EXTENSION; + case 'input': + return _graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION; + } + }, + [_graphql.Kind.SCHEMA_EXTENSION]: ['SchemaDef'], + [_graphql.Kind.SCALAR_TYPE_EXTENSION]: ['ScalarDef'], + [_graphql.Kind.OBJECT_TYPE_EXTENSION]: ['ObjectTypeDef'], + [_graphql.Kind.INTERFACE_TYPE_EXTENSION]: ['InterfaceDef'], + [_graphql.Kind.UNION_TYPE_EXTENSION]: ['UnionDef'], + [_graphql.Kind.ENUM_TYPE_EXTENSION]: ['EnumDef'], + [_graphql.Kind.INPUT_OBJECT_TYPE_EXTENSION]: ['InputDef'] +}; +exports.ParseRules = ParseRules; +function word(value) { + return { + style: 'keyword', + match: token => token.kind === 'Name' && token.value === value + }; +} +function name(style) { + return { + style, + match: token => token.kind === 'Name', + update(state, token) { + state.name = token.value; + } + }; +} +function type(style) { + return { + style, + match: token => token.kind === 'Name', + update(state, token) { + var _a; + if ((_a = state.prevState) === null || _a === void 0 ? void 0 : _a.prevState) { + state.name = token.value; + state.prevState.prevState.type = token.value; + } + } + }; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/index.js": +/*!**********************************************************!*\ + !*** ../../graphql-language-service/esm/parser/index.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _exportNames = { + CharacterStream: true, + LexRules: true, + ParseRules: true, + isIgnored: true, + butNot: true, + list: true, + opt: true, + p: true, + t: true, + onlineParser: true +}; +Object.defineProperty(exports, "CharacterStream", ({ + enumerable: true, + get: function () { + return _CharacterStream.default; + } +})); +Object.defineProperty(exports, "LexRules", ({ + enumerable: true, + get: function () { + return _Rules.LexRules; + } +})); +Object.defineProperty(exports, "ParseRules", ({ + enumerable: true, + get: function () { + return _Rules.ParseRules; + } +})); +Object.defineProperty(exports, "butNot", ({ + enumerable: true, + get: function () { + return _RuleHelpers.butNot; + } +})); +Object.defineProperty(exports, "isIgnored", ({ + enumerable: true, + get: function () { + return _Rules.isIgnored; + } +})); +Object.defineProperty(exports, "list", ({ + enumerable: true, + get: function () { + return _RuleHelpers.list; + } +})); +Object.defineProperty(exports, "onlineParser", ({ + enumerable: true, + get: function () { + return _onlineParser.default; + } +})); +Object.defineProperty(exports, "opt", ({ + enumerable: true, + get: function () { + return _RuleHelpers.opt; + } +})); +Object.defineProperty(exports, "p", ({ + enumerable: true, + get: function () { + return _RuleHelpers.p; + } +})); +Object.defineProperty(exports, "t", ({ + enumerable: true, + get: function () { + return _RuleHelpers.t; + } +})); +var _CharacterStream = _interopRequireDefault(__webpack_require__(/*! ./CharacterStream */ "../../graphql-language-service/esm/parser/CharacterStream.js")); +var _Rules = __webpack_require__(/*! ./Rules */ "../../graphql-language-service/esm/parser/Rules.js"); +var _RuleHelpers = __webpack_require__(/*! ./RuleHelpers */ "../../graphql-language-service/esm/parser/RuleHelpers.js"); +var _onlineParser = _interopRequireDefault(__webpack_require__(/*! ./onlineParser */ "../../graphql-language-service/esm/parser/onlineParser.js")); +var _types = __webpack_require__(/*! ./types */ "../../graphql-language-service/esm/parser/types.js"); +Object.keys(_types).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _types[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _types[key]; + } + }); +}); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/onlineParser.js": +/*!*****************************************************************!*\ + !*** ../../graphql-language-service/esm/parser/onlineParser.js ***! + \*****************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = onlineParser; +var _Rules = __webpack_require__(/*! ./Rules */ "../../graphql-language-service/esm/parser/Rules.js"); +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function onlineParser() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { + eatWhitespace: stream => stream.eatWhile(_Rules.isIgnored), + lexRules: _Rules.LexRules, + parseRules: _Rules.ParseRules, + editorConfig: {} + }; + return { + startState() { + const initialState = { + level: 0, + step: 0, + name: null, + kind: null, + type: null, + rule: null, + needsSeparator: false, + prevState: null + }; + pushRule(options.parseRules, initialState, _graphql.Kind.DOCUMENT); + return initialState; + }, + token(stream, state) { + return getToken(stream, state, options); + } + }; +} +function getToken(stream, state, options) { + var _a; + if (state.inBlockstring) { + if (stream.match(/.*"""/)) { + state.inBlockstring = false; + return 'string'; + } + stream.skipToEnd(); + return 'string'; + } + const { + lexRules, + parseRules, + eatWhitespace, + editorConfig + } = options; + if (state.rule && state.rule.length === 0) { + popRule(state); + } else if (state.needsAdvance) { + state.needsAdvance = false; + advanceRule(state, true); + } + if (stream.sol()) { + const tabSize = (editorConfig === null || editorConfig === void 0 ? void 0 : editorConfig.tabSize) || 2; + state.indentLevel = Math.floor(stream.indentation() / tabSize); + } + if (eatWhitespace(stream)) { + return 'ws'; + } + const token = lex(lexRules, stream); + if (!token) { + const matchedSomething = stream.match(/\S+/); + if (!matchedSomething) { + stream.match(/\s/); + } + pushRule(SpecialParseRules, state, 'Invalid'); + return 'invalidchar'; + } + if (token.kind === 'Comment') { + pushRule(SpecialParseRules, state, 'Comment'); + return 'comment'; + } + const backupState = assign({}, state); + if (token.kind === 'Punctuation') { + if (/^[{([]/.test(token.value)) { + if (state.indentLevel !== undefined) { + state.levels = (state.levels || []).concat(state.indentLevel + 1); + } + } else if (/^[})\]]/.test(token.value)) { + const levels = state.levels = (state.levels || []).slice(0, -1); + if (state.indentLevel && levels.length > 0 && levels.at(-1) < state.indentLevel) { + state.indentLevel = levels.at(-1); + } + } + } + while (state.rule) { + let expected = typeof state.rule === 'function' ? state.step === 0 ? state.rule(token, stream) : null : state.rule[state.step]; + if (state.needsSeparator) { + expected = expected === null || expected === void 0 ? void 0 : expected.separator; + } + if (expected) { + if (expected.ofRule) { + expected = expected.ofRule; + } + if (typeof expected === 'string') { + pushRule(parseRules, state, expected); + continue; + } + if ((_a = expected.match) === null || _a === void 0 ? void 0 : _a.call(expected, token)) { + if (expected.update) { + expected.update(state, token); + } + if (token.kind === 'Punctuation') { + advanceRule(state, true); + } else { + state.needsAdvance = true; + } + return expected.style; + } + } + unsuccessful(state); + } + assign(state, backupState); + pushRule(SpecialParseRules, state, 'Invalid'); + return 'invalidchar'; +} +function assign(to, from) { + const keys = Object.keys(from); + for (let i = 0; i < keys.length; i++) { + to[keys[i]] = from[keys[i]]; + } + return to; +} +const SpecialParseRules = { + Invalid: [], + Comment: [] +}; +function pushRule(rules, state, ruleKind) { + if (!rules[ruleKind]) { + throw new TypeError('Unknown rule: ' + ruleKind); + } + state.prevState = Object.assign({}, state); + state.kind = ruleKind; + state.name = null; + state.type = null; + state.rule = rules[ruleKind]; + state.step = 0; + state.needsSeparator = false; +} +function popRule(state) { + if (!state.prevState) { + return; + } + state.kind = state.prevState.kind; + state.name = state.prevState.name; + state.type = state.prevState.type; + state.rule = state.prevState.rule; + state.step = state.prevState.step; + state.needsSeparator = state.prevState.needsSeparator; + state.prevState = state.prevState.prevState; +} +function advanceRule(state, successful) { + var _a; + if (isList(state) && state.rule) { + const step = state.rule[state.step]; + if (step.separator) { + const { + separator + } = step; + state.needsSeparator = !state.needsSeparator; + if (!state.needsSeparator && separator.ofRule) { + return; + } + } + if (successful) { + return; + } + } + state.needsSeparator = false; + state.step++; + while (state.rule && !(Array.isArray(state.rule) && state.step < state.rule.length)) { + popRule(state); + if (state.rule) { + if (isList(state)) { + if ((_a = state.rule) === null || _a === void 0 ? void 0 : _a[state.step].separator) { + state.needsSeparator = !state.needsSeparator; + } + } else { + state.needsSeparator = false; + state.step++; + } + } + } +} +function isList(state) { + const step = Array.isArray(state.rule) && typeof state.rule[state.step] !== 'string' && state.rule[state.step]; + return step && step.isList; +} +function unsuccessful(state) { + while (state.rule && !(Array.isArray(state.rule) && state.rule[state.step].ofRule)) { + popRule(state); + } + if (state.rule) { + advanceRule(state, false); + } +} +function lex(lexRules, stream) { + const kinds = Object.keys(lexRules); + for (let i = 0; i < kinds.length; i++) { + const match = stream.match(lexRules[kinds[i]]); + if (match && match instanceof Array) { + return { + kind: kinds[i], + value: match[0] + }; + } + } +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/parser/types.js": +/*!**********************************************************!*\ + !*** ../../graphql-language-service/esm/parser/types.js ***! + \**********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RuleKinds = exports.AdditionalRuleKinds = void 0; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +const AdditionalRuleKinds = { + ALIASED_FIELD: 'AliasedField', + ARGUMENTS: 'Arguments', + SHORT_QUERY: 'ShortQuery', + QUERY: 'Query', + MUTATION: 'Mutation', + SUBSCRIPTION: 'Subscription', + TYPE_CONDITION: 'TypeCondition', + INVALID: 'Invalid', + COMMENT: 'Comment', + SCHEMA_DEF: 'SchemaDef', + SCALAR_DEF: 'ScalarDef', + OBJECT_TYPE_DEF: 'ObjectTypeDef', + OBJECT_VALUE: 'ObjectValue', + LIST_VALUE: 'ListValue', + INTERFACE_DEF: 'InterfaceDef', + UNION_DEF: 'UnionDef', + ENUM_DEF: 'EnumDef', + ENUM_VALUE: 'EnumValue', + FIELD_DEF: 'FieldDef', + INPUT_DEF: 'InputDef', + INPUT_VALUE_DEF: 'InputValueDef', + ARGUMENTS_DEF: 'ArgumentsDef', + EXTEND_DEF: 'ExtendDef', + EXTENSION_DEFINITION: 'ExtensionDefinition', + DIRECTIVE_DEF: 'DirectiveDef', + IMPLEMENTS: 'Implements', + VARIABLE_DEFINITIONS: 'VariableDefinitions', + TYPE: 'Type' +}; +exports.AdditionalRuleKinds = AdditionalRuleKinds; +const RuleKinds = Object.assign(Object.assign({}, _graphql.Kind), AdditionalRuleKinds); +exports.RuleKinds = RuleKinds; + +/***/ }), + +/***/ "../../graphql-language-service/esm/types.js": +/*!***************************************************!*\ + !*** ../../graphql-language-service/esm/types.js ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.FileChangeTypeKind = exports.CompletionItemKind = void 0; +Object.defineProperty(exports, "InsertTextFormat", ({ + enumerable: true, + get: function () { + return _vscodeLanguageserverTypes.InsertTextFormat; + } +})); +var _vscodeLanguageserverTypes = __webpack_require__(/*! vscode-languageserver-types */ "../../../node_modules/vscode-languageserver-types/lib/esm/main.js"); +const FileChangeTypeKind = { + Created: 1, + Changed: 2, + Deleted: 3 +}; +exports.FileChangeTypeKind = FileChangeTypeKind; +var CompletionItemKind; +exports.CompletionItemKind = CompletionItemKind; +(function (CompletionItemKind) { + CompletionItemKind.Text = 1; + CompletionItemKind.Method = 2; + CompletionItemKind.Function = 3; + CompletionItemKind.Constructor = 4; + CompletionItemKind.Field = 5; + CompletionItemKind.Variable = 6; + CompletionItemKind.Class = 7; + CompletionItemKind.Interface = 8; + CompletionItemKind.Module = 9; + CompletionItemKind.Property = 10; + CompletionItemKind.Unit = 11; + CompletionItemKind.Value = 12; + CompletionItemKind.Enum = 13; + CompletionItemKind.Keyword = 14; + CompletionItemKind.Snippet = 15; + CompletionItemKind.Color = 16; + CompletionItemKind.File = 17; + CompletionItemKind.Reference = 18; + CompletionItemKind.Folder = 19; + CompletionItemKind.EnumMember = 20; + CompletionItemKind.Constant = 21; + CompletionItemKind.Struct = 22; + CompletionItemKind.Event = 23; + CompletionItemKind.Operator = 24; + CompletionItemKind.TypeParameter = 25; +})(CompletionItemKind || (exports.CompletionItemKind = CompletionItemKind = {})); + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/Range.js": +/*!*********************************************************!*\ + !*** ../../graphql-language-service/esm/utils/Range.js ***! + \*********************************************************/ +/***/ (function(__unused_webpack_module, exports) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Range = exports.Position = void 0; +exports.locToRange = locToRange; +exports.offsetToPosition = offsetToPosition; +class Range { + constructor(start, end) { + this.containsPosition = position => { + if (this.start.line === position.line) { + return this.start.character <= position.character; + } + if (this.end.line === position.line) { + return this.end.character >= position.character; + } + return this.start.line <= position.line && this.end.line >= position.line; + }; + this.start = start; + this.end = end; + } + setStart(line, character) { + this.start = new Position(line, character); + } + setEnd(line, character) { + this.end = new Position(line, character); + } +} +exports.Range = Range; +class Position { + constructor(line, character) { + this.lessThanOrEqualTo = position => this.line < position.line || this.line === position.line && this.character <= position.character; + this.line = line; + this.character = character; + } + setLine(line) { + this.line = line; + } + setCharacter(character) { + this.character = character; + } +} +exports.Position = Position; +function offsetToPosition(text, loc) { + const EOL = '\n'; + const buf = text.slice(0, loc); + const lines = buf.split(EOL).length - 1; + const lastLineIndex = buf.lastIndexOf(EOL); + return new Position(lines, loc - lastLineIndex - 1); +} +function locToRange(text, loc) { + const start = offsetToPosition(text, loc.start); + const end = offsetToPosition(text, loc.end); + return new Range(start, end); +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/collectVariables.js": +/*!********************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/collectVariables.js ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.collectVariables = collectVariables; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function collectVariables(schema, documentAST) { + const variableToType = Object.create(null); + for (const definition of documentAST.definitions) { + if (definition.kind === 'OperationDefinition') { + const { + variableDefinitions + } = definition; + if (variableDefinitions) { + for (const { + variable, + type + } of variableDefinitions) { + const inputType = (0, _graphql.typeFromAST)(schema, type); + if (inputType) { + variableToType[variable.name.value] = inputType; + } else if (type.kind === _graphql.Kind.NAMED_TYPE && type.name.value === 'Float') { + variableToType[variable.name.value] = _graphql.GraphQLFloat; + } + } + } + } + } + return variableToType; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/fragmentDependencies.js": +/*!************************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/fragmentDependencies.js ***! + \************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getFragmentDependenciesForAST = exports.getFragmentDependencies = void 0; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _nullthrows = _interopRequireDefault(__webpack_require__(/*! nullthrows */ "../../../node_modules/nullthrows/nullthrows.js")); +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +const getFragmentDependencies = (operationString, fragmentDefinitions) => { + if (!fragmentDefinitions) { + return []; + } + let parsedOperation; + try { + parsedOperation = (0, _graphql.parse)(operationString); + } catch (_a) { + return []; + } + return getFragmentDependenciesForAST(parsedOperation, fragmentDefinitions); +}; +exports.getFragmentDependencies = getFragmentDependencies; +const getFragmentDependenciesForAST = (parsedOperation, fragmentDefinitions) => { + if (!fragmentDefinitions) { + return []; + } + const existingFrags = new Map(); + const referencedFragNames = new Set(); + (0, _graphql.visit)(parsedOperation, { + FragmentDefinition(node) { + existingFrags.set(node.name.value, true); + }, + FragmentSpread(node) { + if (!referencedFragNames.has(node.name.value)) { + referencedFragNames.add(node.name.value); + } + } + }); + const asts = new Set(); + for (const name of referencedFragNames) { + if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { + asts.add((0, _nullthrows.default)(fragmentDefinitions.get(name))); + } + } + const referencedFragments = []; + for (const ast of asts) { + (0, _graphql.visit)(ast, { + FragmentSpread(node) { + if (!referencedFragNames.has(node.name.value) && fragmentDefinitions.get(node.name.value)) { + asts.add((0, _nullthrows.default)(fragmentDefinitions.get(node.name.value))); + referencedFragNames.add(node.name.value); + } + } + }); + if (!existingFrags.has(ast.name.value)) { + referencedFragments.push(ast); + } + } + return referencedFragments; +}; +exports.getFragmentDependenciesForAST = getFragmentDependenciesForAST; + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/getASTNodeAtPosition.js": +/*!************************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/getASTNodeAtPosition.js ***! + \************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getASTNodeAtPosition = getASTNodeAtPosition; +exports.pointToOffset = pointToOffset; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +function getASTNodeAtPosition(query, ast, point) { + const offset = pointToOffset(query, point); + let nodeContainingPosition; + (0, _graphql.visit)(ast, { + enter(node) { + if (node.kind !== 'Name' && node.loc && node.loc.start <= offset && offset <= node.loc.end) { + nodeContainingPosition = node; + } else { + return false; + } + }, + leave(node) { + if (node.loc && node.loc.start <= offset && offset <= node.loc.end) { + return false; + } + } + }); + return nodeContainingPosition; +} +function pointToOffset(text, point) { + const linesUntilPosition = text.split('\n').slice(0, point.line); + return point.character + linesUntilPosition.map(line => line.length + 1).reduce((a, b) => a + b, 0); +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/getOperationFacts.js": +/*!*********************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/getOperationFacts.js ***! + \*********************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = getOperationFacts; +exports.getOperationASTFacts = getOperationASTFacts; +exports.getQueryFacts = void 0; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +var _collectVariables = __webpack_require__(/*! ./collectVariables */ "../../graphql-language-service/esm/utils/collectVariables.js"); +function getOperationASTFacts(documentAST, schema) { + const variableToType = schema ? (0, _collectVariables.collectVariables)(schema, documentAST) : undefined; + const operations = []; + (0, _graphql.visit)(documentAST, { + OperationDefinition(node) { + operations.push(node); + } + }); + return { + variableToType, + operations + }; +} +function getOperationFacts(schema, documentString) { + if (!documentString) { + return; + } + try { + const documentAST = (0, _graphql.parse)(documentString); + return Object.assign(Object.assign({}, getOperationASTFacts(documentAST, schema)), { + documentAST + }); + } catch (_a) { + return; + } +} +const getQueryFacts = getOperationFacts; +exports.getQueryFacts = getQueryFacts; + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/getVariablesJSONSchema.js": +/*!**************************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/getVariablesJSONSchema.js ***! + \**************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.defaultJSONSchemaOptions = void 0; +exports.getVariablesJSONSchema = getVariablesJSONSchema; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +const defaultJSONSchemaOptions = { + useMarkdownDescription: false +}; +exports.defaultJSONSchemaOptions = defaultJSONSchemaOptions; +function text(into, newText) { + into.push(newText); +} +function renderType(into, t) { + if ((0, _graphql.isNonNullType)(t)) { + renderType(into, t.ofType); + text(into, '!'); + } else if ((0, _graphql.isListType)(t)) { + text(into, '['); + renderType(into, t.ofType); + text(into, ']'); + } else { + text(into, t.name); + } +} +function renderTypeToString(t, useMarkdown) { + const into = []; + if (useMarkdown) { + text(into, '```graphql\n'); + } + renderType(into, t); + if (useMarkdown) { + text(into, '\n```'); + } + return into.join(''); +} +const scalarTypesMap = { + Int: 'integer', + String: 'string', + Float: 'number', + ID: 'string', + Boolean: 'boolean', + DateTime: 'string' +}; +class Marker { + constructor() { + this.set = new Set(); + } + mark(name) { + if (this.set.has(name)) { + return false; + } + this.set.add(name); + return true; + } +} +function getJSONSchemaFromGraphQLType(type, options) { + let required = false; + let definition = Object.create(null); + const definitions = Object.create(null); + if ('defaultValue' in type && type.defaultValue !== undefined) { + definition.default = type.defaultValue; + } + if ((0, _graphql.isEnumType)(type)) { + definition.type = 'string'; + definition.enum = type.getValues().map(val => val.name); + } + if ((0, _graphql.isScalarType)(type) && scalarTypesMap[type.name]) { + definition.type = scalarTypesMap[type.name]; + } + if ((0, _graphql.isListType)(type)) { + definition.type = 'array'; + const { + definition: def, + definitions: defs + } = getJSONSchemaFromGraphQLType(type.ofType, options); + if (def.$ref) { + definition.items = { + $ref: def.$ref + }; + } else { + definition.items = def; + } + if (defs) { + for (const defName of Object.keys(defs)) { + definitions[defName] = defs[defName]; + } + } + } + if ((0, _graphql.isNonNullType)(type)) { + required = true; + const { + definition: def, + definitions: defs + } = getJSONSchemaFromGraphQLType(type.ofType, options); + definition = def; + if (defs) { + for (const defName of Object.keys(defs)) { + definitions[defName] = defs[defName]; + } + } + } + if ((0, _graphql.isInputObjectType)(type)) { + definition.$ref = `#/definitions/${type.name}`; + if (options === null || options === void 0 ? void 0 : options.definitionMarker.mark(type.name)) { + const fields = type.getFields(); + const fieldDef = { + type: 'object', + properties: {}, + required: [] + }; + if (type.description) { + fieldDef.description = type.description + '\n' + renderTypeToString(type); + if (options === null || options === void 0 ? void 0 : options.useMarkdownDescription) { + fieldDef.markdownDescription = type.description + '\n' + renderTypeToString(type, true); + } + } else { + fieldDef.description = renderTypeToString(type); + if (options === null || options === void 0 ? void 0 : options.useMarkdownDescription) { + fieldDef.markdownDescription = renderTypeToString(type, true); + } + } + for (const fieldName of Object.keys(fields)) { + const field = fields[fieldName]; + const { + required: fieldRequired, + definition: typeDefinition, + definitions: typeDefinitions + } = getJSONSchemaFromGraphQLType(field.type, options); + const { + definition: fieldDefinition + } = getJSONSchemaFromGraphQLType(field, options); + fieldDef.properties[fieldName] = Object.assign(Object.assign({}, typeDefinition), fieldDefinition); + const renderedField = renderTypeToString(field.type); + fieldDef.properties[fieldName].description = field.description ? field.description + '\n' + renderedField : renderedField; + if (options === null || options === void 0 ? void 0 : options.useMarkdownDescription) { + const renderedFieldMarkdown = renderTypeToString(field.type, true); + fieldDef.properties[fieldName].markdownDescription = field.description ? field.description + '\n' + renderedFieldMarkdown : renderedFieldMarkdown; + } + if (fieldRequired) { + fieldDef.required.push(fieldName); + } + if (typeDefinitions) { + for (const [defName, value] of Object.entries(typeDefinitions)) { + definitions[defName] = value; + } + } + } + definitions[type.name] = fieldDef; + } + } + if ('description' in type && !(0, _graphql.isScalarType)(type) && type.description && !definition.description) { + definition.description = type.description + '\n' + renderTypeToString(type); + if (options === null || options === void 0 ? void 0 : options.useMarkdownDescription) { + definition.markdownDescription = type.description + '\n' + renderTypeToString(type, true); + } + } else { + definition.description = renderTypeToString(type); + if (options === null || options === void 0 ? void 0 : options.useMarkdownDescription) { + definition.markdownDescription = renderTypeToString(type, true); + } + } + return { + required, + definition, + definitions + }; +} +function getVariablesJSONSchema(variableToType, options) { + var _a; + const jsonSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: {}, + required: [] + }; + const runtimeOptions = Object.assign(Object.assign({}, options), { + definitionMarker: new Marker() + }); + if (variableToType) { + for (const [variableName, type] of Object.entries(variableToType)) { + const { + definition, + required, + definitions + } = getJSONSchemaFromGraphQLType(type, runtimeOptions); + jsonSchema.properties[variableName] = definition; + if (required) { + (_a = jsonSchema.required) === null || _a === void 0 ? void 0 : _a.push(variableName); + } + if (definitions) { + jsonSchema.definitions = Object.assign(Object.assign({}, jsonSchema === null || jsonSchema === void 0 ? void 0 : jsonSchema.definitions), definitions); + } + } + } + return jsonSchema; +} + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/index.js": +/*!*********************************************************!*\ + !*** ../../graphql-language-service/esm/utils/index.js ***! + \*********************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "Position", ({ + enumerable: true, + get: function () { + return _Range.Position; + } +})); +Object.defineProperty(exports, "Range", ({ + enumerable: true, + get: function () { + return _Range.Range; + } +})); +Object.defineProperty(exports, "collectVariables", ({ + enumerable: true, + get: function () { + return _collectVariables.collectVariables; + } +})); +Object.defineProperty(exports, "getASTNodeAtPosition", ({ + enumerable: true, + get: function () { + return _getASTNodeAtPosition.getASTNodeAtPosition; + } +})); +Object.defineProperty(exports, "getFragmentDependencies", ({ + enumerable: true, + get: function () { + return _fragmentDependencies.getFragmentDependencies; + } +})); +Object.defineProperty(exports, "getFragmentDependenciesForAST", ({ + enumerable: true, + get: function () { + return _fragmentDependencies.getFragmentDependenciesForAST; + } +})); +Object.defineProperty(exports, "getOperationASTFacts", ({ + enumerable: true, + get: function () { + return _getOperationFacts.getOperationASTFacts; + } +})); +Object.defineProperty(exports, "getOperationFacts", ({ + enumerable: true, + get: function () { + return _getOperationFacts.default; + } +})); +Object.defineProperty(exports, "getQueryFacts", ({ + enumerable: true, + get: function () { + return _getOperationFacts.getQueryFacts; + } +})); +Object.defineProperty(exports, "getVariablesJSONSchema", ({ + enumerable: true, + get: function () { + return _getVariablesJSONSchema.getVariablesJSONSchema; + } +})); +Object.defineProperty(exports, "locToRange", ({ + enumerable: true, + get: function () { + return _Range.locToRange; + } +})); +Object.defineProperty(exports, "offsetToPosition", ({ + enumerable: true, + get: function () { + return _Range.offsetToPosition; + } +})); +Object.defineProperty(exports, "pointToOffset", ({ + enumerable: true, + get: function () { + return _getASTNodeAtPosition.pointToOffset; + } +})); +Object.defineProperty(exports, "validateWithCustomRules", ({ + enumerable: true, + get: function () { + return _validateWithCustomRules.validateWithCustomRules; + } +})); +var _fragmentDependencies = __webpack_require__(/*! ./fragmentDependencies */ "../../graphql-language-service/esm/utils/fragmentDependencies.js"); +var _getVariablesJSONSchema = __webpack_require__(/*! ./getVariablesJSONSchema */ "../../graphql-language-service/esm/utils/getVariablesJSONSchema.js"); +var _getASTNodeAtPosition = __webpack_require__(/*! ./getASTNodeAtPosition */ "../../graphql-language-service/esm/utils/getASTNodeAtPosition.js"); +var _Range = __webpack_require__(/*! ./Range */ "../../graphql-language-service/esm/utils/Range.js"); +var _validateWithCustomRules = __webpack_require__(/*! ./validateWithCustomRules */ "../../graphql-language-service/esm/utils/validateWithCustomRules.js"); +var _collectVariables = __webpack_require__(/*! ./collectVariables */ "../../graphql-language-service/esm/utils/collectVariables.js"); +var _getOperationFacts = _interopRequireWildcard(__webpack_require__(/*! ./getOperationFacts */ "../../graphql-language-service/esm/utils/getOperationFacts.js")); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +/***/ }), + +/***/ "../../graphql-language-service/esm/utils/validateWithCustomRules.js": +/*!***************************************************************************!*\ + !*** ../../graphql-language-service/esm/utils/validateWithCustomRules.js ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.validateWithCustomRules = validateWithCustomRules; +var _graphql = __webpack_require__(/*! graphql */ "../../../node_modules/graphql/index.mjs"); +const specifiedSDLRules = [_graphql.LoneSchemaDefinitionRule, _graphql.UniqueOperationTypesRule, _graphql.UniqueTypeNamesRule, _graphql.UniqueEnumValueNamesRule, _graphql.UniqueFieldDefinitionNamesRule, _graphql.UniqueDirectiveNamesRule, _graphql.KnownTypeNamesRule, _graphql.KnownDirectivesRule, _graphql.UniqueDirectivesPerLocationRule, _graphql.PossibleTypeExtensionsRule, _graphql.UniqueArgumentNamesRule, _graphql.UniqueInputFieldNamesRule]; +function validateWithCustomRules(schema, ast, customRules, isRelayCompatMode, isSchemaDocument) { + const rules = _graphql.specifiedRules.filter(rule => { + if (rule === _graphql.NoUnusedFragmentsRule || rule === _graphql.ExecutableDefinitionsRule) { + return false; + } + if (isRelayCompatMode && rule === _graphql.KnownFragmentNamesRule) { + return false; + } + return true; + }); + if (customRules) { + Array.prototype.push.apply(rules, customRules); + } + if (isSchemaDocument) { + Array.prototype.push.apply(rules, specifiedSDLRules); + } + const errors = (0, _graphql.validate)(schema, ast, rules); + return errors.filter(error => { + if (error.message.includes('Unknown directive') && error.nodes) { + const node = error.nodes[0]; + if (node && node.kind === _graphql.Kind.DIRECTIVE) { + const name = node.name.value; + if (name === 'arguments' || name === 'argumentDefinitions') { + return false; + } + } + } + return true; + }); +} + +/***/ }), + +/***/ "./style.css": +/*!*******************!*\ + !*** ./style.css ***! + \*******************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + +/***/ }), + +/***/ "../../graphiql-react/dist/style.css": +/*!*******************************************!*\ + !*** ../../graphiql-react/dist/style.css ***! + \*******************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + +/***/ }), + +/***/ "../../graphiql-react/font/fira-code.css": +/*!***********************************************!*\ + !*** ../../graphiql-react/font/fira-code.css ***! + \***********************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + +/***/ }), + +/***/ "../../graphiql-react/font/roboto.css": +/*!********************************************!*\ + !*** ../../graphiql-react/font/roboto.css ***! + \********************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + +/***/ }), + +/***/ "react": +/*!************************!*\ + !*** external "React" ***! + \************************/ +/***/ (function(module) { + +module.exports = window["React"]; + +/***/ }), + +/***/ "react-dom": +/*!***************************!*\ + !*** external "ReactDOM" ***! + \***************************/ +/***/ (function(module) { + +module.exports = window["ReactDOM"]; + +/***/ }), + +/***/ "../../../node_modules/@headlessui/react/dist/headlessui.dev.cjs": +/*!***********************************************************************!*\ + !*** ../../../node_modules/@headlessui/react/dist/headlessui.dev.cjs ***! + \***********************************************************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; +}; + +// src/index.ts +var src_exports = {}; +__export(src_exports, { + Combobox: () => Combobox, + Dialog: () => Dialog, + Disclosure: () => Disclosure, + FocusTrap: () => FocusTrap, + Listbox: () => Listbox, + Menu: () => Menu, + Popover: () => Popover, + Portal: () => Portal, + RadioGroup: () => RadioGroup, + Switch: () => Switch, + Tab: () => Tab, + Transition: () => Transition +}); +module.exports = __toCommonJS(src_exports); + +// src/components/combobox/combobox.tsx +var import_react19 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/hooks/use-computed.ts +var import_react3 = __webpack_require__(/*! react */ "react"); + +// src/hooks/use-iso-morphic-effect.ts +var import_react = __webpack_require__(/*! react */ "react"); + +// src/utils/env.ts +var Env = class { + constructor() { + __publicField(this, "current", this.detect()); + __publicField(this, "handoffState", "pending"); + __publicField(this, "currentId", 0); + } + set(env2) { + if (this.current === env2) + return; + this.handoffState = "pending"; + this.currentId = 0; + this.current = env2; + } + reset() { + this.set(this.detect()); + } + nextId() { + return ++this.currentId; + } + get isServer() { + return this.current === "server"; + } + get isClient() { + return this.current === "client"; + } + detect() { + if (typeof window === "undefined" || typeof document === "undefined") { + return "server"; + } + return "client"; + } + handoff() { + if (this.handoffState === "pending") { + this.handoffState = "complete"; + } + } + get isHandoffComplete() { + return this.handoffState === "complete"; + } +}; +var env = new Env(); + +// src/hooks/use-iso-morphic-effect.ts +var useIsoMorphicEffect = (effect, deps) => { + if (env.isServer) { + (0, import_react.useEffect)(effect, deps); + } else { + (0, import_react.useLayoutEffect)(effect, deps); + } +}; + +// src/hooks/use-latest-value.ts +var import_react2 = __webpack_require__(/*! react */ "react"); +function useLatestValue(value) { + let cache = (0, import_react2.useRef)(value); + useIsoMorphicEffect(() => { + cache.current = value; + }, [value]); + return cache; +} + +// src/hooks/use-computed.ts +function useComputed(cb, dependencies) { + let [value, setValue] = (0, import_react3.useState)(cb); + let cbRef = useLatestValue(cb); + useIsoMorphicEffect(() => setValue(cbRef.current), [cbRef, setValue, ...dependencies]); + return value; +} + +// src/hooks/use-disposables.ts +var import_react4 = __webpack_require__(/*! react */ "react"); + +// src/utils/micro-task.ts +function microTask(cb) { + if (typeof queueMicrotask === "function") { + queueMicrotask(cb); + } else { + Promise.resolve().then(cb).catch( + (e) => setTimeout(() => { + throw e; + }) + ); + } +} + +// src/utils/disposables.ts +function disposables() { + let _disposables = []; + let api = { + addEventListener(element, name, listener, options) { + element.addEventListener(name, listener, options); + return api.add(() => element.removeEventListener(name, listener, options)); + }, + requestAnimationFrame(...args) { + let raf = requestAnimationFrame(...args); + return api.add(() => cancelAnimationFrame(raf)); + }, + nextFrame(...args) { + return api.requestAnimationFrame(() => { + return api.requestAnimationFrame(...args); + }); + }, + setTimeout(...args) { + let timer = setTimeout(...args); + return api.add(() => clearTimeout(timer)); + }, + microTask(...args) { + let task = { current: true }; + microTask(() => { + if (task.current) { + args[0](); + } + }); + return api.add(() => { + task.current = false; + }); + }, + style(node, property, value) { + let previous = node.style.getPropertyValue(property); + Object.assign(node.style, { [property]: value }); + return this.add(() => { + Object.assign(node.style, { [property]: previous }); + }); + }, + group(cb) { + let d = disposables(); + cb(d); + return this.add(() => d.dispose()); + }, + add(cb) { + _disposables.push(cb); + return () => { + let idx = _disposables.indexOf(cb); + if (idx >= 0) { + for (let dispose of _disposables.splice(idx, 1)) { + dispose(); + } + } + }; + }, + dispose() { + for (let dispose of _disposables.splice(0)) { + dispose(); + } + } + }; + return api; +} + +// src/hooks/use-disposables.ts +function useDisposables() { + let [d] = (0, import_react4.useState)(disposables); + (0, import_react4.useEffect)(() => () => d.dispose(), [d]); + return d; +} + +// src/hooks/use-event.ts +var import_react5 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var useEvent = ( + // TODO: Add React.useEvent ?? once the useEvent hook is available + function useEvent2(cb) { + let cache = useLatestValue(cb); + return import_react5.default.useCallback((...args) => cache.current(...args), [cache]); + } +); + +// src/hooks/use-id.ts +var import_react7 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/hooks/use-server-handoff-complete.ts +var import_react6 = __webpack_require__(/*! react */ "react"); +function useServerHandoffComplete() { + let [complete, setComplete] = (0, import_react6.useState)(env.isHandoffComplete); + if (complete && env.isHandoffComplete === false) { + setComplete(false); + } + (0, import_react6.useEffect)(() => { + if (complete === true) + return; + setComplete(true); + }, [complete]); + (0, import_react6.useEffect)(() => env.handoff(), []); + return complete; +} + +// src/hooks/use-id.ts +var _a; +var useId = ( + // Prefer React's `useId` if it's available. + // @ts-expect-error - `useId` doesn't exist in React < 18. + (_a = import_react7.default.useId) != null ? _a : function useId2() { + let ready = useServerHandoffComplete(); + let [id, setId] = import_react7.default.useState(ready ? () => env.nextId() : null); + useIsoMorphicEffect(() => { + if (id === null) + setId(env.nextId()); + }, [id]); + return id != null ? "" + id : void 0; + } +); + +// src/hooks/use-outside-click.ts +var import_react10 = __webpack_require__(/*! react */ "react"); + +// src/utils/match.ts +function match(value, lookup, ...args) { + if (value in lookup) { + let returnValue = lookup[value]; + return typeof returnValue === "function" ? returnValue(...args) : returnValue; + } + let error = new Error( + `Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys( + lookup + ).map((key) => `"${key}"`).join(", ")}.` + ); + if (Error.captureStackTrace) + Error.captureStackTrace(error, match); + throw error; +} + +// src/utils/owner.ts +function getOwnerDocument(element) { + if (env.isServer) + return null; + if (element instanceof Node) + return element.ownerDocument; + if (element == null ? void 0 : element.hasOwnProperty("current")) { + if (element.current instanceof Node) + return element.current.ownerDocument; + } + return document; +} + +// src/utils/focus-management.ts +var focusableSelector = [ + "[contentEditable=true]", + "[tabindex]", + "a[href]", + "area[href]", + "button:not([disabled])", + "iframe", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])" +].map( + false ? ( + // TODO: Remove this once JSDOM fixes the issue where an element that is + // "hidden" can be the document.activeElement, because this is not possible + // in real browsers. + 0 + ) : (selector) => `${selector}:not([tabindex='-1'])` +).join(","); +function getFocusableElements(container = document.body) { + if (container == null) + return []; + return Array.from(container.querySelectorAll(focusableSelector)).sort( + // We want to move `tabIndex={0}` to the end of the list, this is what the browser does as well. + (a, z) => Math.sign((a.tabIndex || Number.MAX_SAFE_INTEGER) - (z.tabIndex || Number.MAX_SAFE_INTEGER)) + ); +} +function isFocusableElement(element, mode = 0 /* Strict */) { + var _a3; + if (element === ((_a3 = getOwnerDocument(element)) == null ? void 0 : _a3.body)) + return false; + return match(mode, { + [0 /* Strict */]() { + return element.matches(focusableSelector); + }, + [1 /* Loose */]() { + let next = element; + while (next !== null) { + if (next.matches(focusableSelector)) + return true; + next = next.parentElement; + } + return false; + } + }); +} +function restoreFocusIfNecessary(element) { + let ownerDocument = getOwnerDocument(element); + disposables().nextFrame(() => { + if (ownerDocument && !isFocusableElement(ownerDocument.activeElement, 0 /* Strict */)) { + focusElement(element); + } + }); +} +if (typeof window !== "undefined" && typeof document !== "undefined") { + document.addEventListener( + "keydown", + (event) => { + if (event.metaKey || event.altKey || event.ctrlKey) { + return; + } + document.documentElement.dataset.headlessuiFocusVisible = ""; + }, + true + ); + document.addEventListener( + "click", + (event) => { + if (event.detail === 1 /* Mouse */) { + delete document.documentElement.dataset.headlessuiFocusVisible; + } else if (event.detail === 0 /* Keyboard */) { + document.documentElement.dataset.headlessuiFocusVisible = ""; + } + }, + true + ); +} +function focusElement(element) { + element == null ? void 0 : element.focus({ preventScroll: true }); +} +var selectableSelector = ["textarea", "input"].join(","); +function isSelectableElement(element) { + var _a3, _b; + return (_b = (_a3 = element == null ? void 0 : element.matches) == null ? void 0 : _a3.call(element, selectableSelector)) != null ? _b : false; +} +function sortByDomNode(nodes, resolveKey = (i) => i) { + return nodes.slice().sort((aItem, zItem) => { + let a = resolveKey(aItem); + let z = resolveKey(zItem); + if (a === null || z === null) + return 0; + let position = a.compareDocumentPosition(z); + if (position & Node.DOCUMENT_POSITION_FOLLOWING) + return -1; + if (position & Node.DOCUMENT_POSITION_PRECEDING) + return 1; + return 0; + }); +} +function focusFrom(current, focus) { + return focusIn(getFocusableElements(), focus, { relativeTo: current }); +} +function focusIn(container, focus, { + sorted = true, + relativeTo = null, + skipElements = [] +} = {}) { + let ownerDocument = Array.isArray(container) ? container.length > 0 ? container[0].ownerDocument : document : container.ownerDocument; + let elements = Array.isArray(container) ? sorted ? sortByDomNode(container) : container : getFocusableElements(container); + if (skipElements.length > 0 && elements.length > 1) { + elements = elements.filter((x) => !skipElements.includes(x)); + } + relativeTo = relativeTo != null ? relativeTo : ownerDocument.activeElement; + let direction = (() => { + if (focus & (1 /* First */ | 4 /* Next */)) + return 1 /* Next */; + if (focus & (2 /* Previous */ | 8 /* Last */)) + return -1 /* Previous */; + throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"); + })(); + let startIndex = (() => { + if (focus & 1 /* First */) + return 0; + if (focus & 2 /* Previous */) + return Math.max(0, elements.indexOf(relativeTo)) - 1; + if (focus & 4 /* Next */) + return Math.max(0, elements.indexOf(relativeTo)) + 1; + if (focus & 8 /* Last */) + return elements.length - 1; + throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"); + })(); + let focusOptions = focus & 32 /* NoScroll */ ? { preventScroll: true } : {}; + let offset = 0; + let total = elements.length; + let next = void 0; + do { + if (offset >= total || offset + total <= 0) + return 0 /* Error */; + let nextIdx = startIndex + offset; + if (focus & 16 /* WrapAround */) { + nextIdx = (nextIdx + total) % total; + } else { + if (nextIdx < 0) + return 3 /* Underflow */; + if (nextIdx >= total) + return 1 /* Overflow */; + } + next = elements[nextIdx]; + next == null ? void 0 : next.focus(focusOptions); + offset += direction; + } while (next !== ownerDocument.activeElement); + if (focus & (4 /* Next */ | 2 /* Previous */) && isSelectableElement(next)) { + next.select(); + } + return 2 /* Success */; +} + +// src/hooks/use-document-event.ts +var import_react8 = __webpack_require__(/*! react */ "react"); +function useDocumentEvent(type, listener, options) { + let listenerRef = useLatestValue(listener); + (0, import_react8.useEffect)(() => { + function handler(event) { + listenerRef.current(event); + } + document.addEventListener(type, handler, options); + return () => document.removeEventListener(type, handler, options); + }, [type, options]); +} + +// src/hooks/use-window-event.ts +var import_react9 = __webpack_require__(/*! react */ "react"); +function useWindowEvent(type, listener, options) { + let listenerRef = useLatestValue(listener); + (0, import_react9.useEffect)(() => { + function handler(event) { + listenerRef.current(event); + } + window.addEventListener(type, handler, options); + return () => window.removeEventListener(type, handler, options); + }, [type, options]); +} + +// src/hooks/use-outside-click.ts +function useOutsideClick(containers, cb, enabled = true) { + let enabledRef = (0, import_react10.useRef)(false); + (0, import_react10.useEffect)( + false ? 0 : () => { + requestAnimationFrame(() => { + enabledRef.current = enabled; + }); + }, + [enabled] + ); + function handleOutsideClick(event, resolveTarget) { + if (!enabledRef.current) + return; + if (event.defaultPrevented) + return; + let target = resolveTarget(event); + if (target === null) { + return; + } + if (!target.getRootNode().contains(target)) + return; + let _containers = function resolve(containers2) { + if (typeof containers2 === "function") { + return resolve(containers2()); + } + if (Array.isArray(containers2)) { + return containers2; + } + if (containers2 instanceof Set) { + return containers2; + } + return [containers2]; + }(containers); + for (let container of _containers) { + if (container === null) + continue; + let domNode = container instanceof HTMLElement ? container : container.current; + if (domNode == null ? void 0 : domNode.contains(target)) { + return; + } + if (event.composed && event.composedPath().includes(domNode)) { + return; + } + } + if ( + // This check alllows us to know whether or not we clicked on a "focusable" element like a + // button or an input. This is a backwards compatibility check so that you can open a and click on another which should close Menu A and open Menu B. We might + // revisit that so that you will require 2 clicks instead. + !isFocusableElement(target, 1 /* Loose */) && // This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it + // unfocusable via the keyboard so that tabbing to the next item from the input doesn't + // first go to the button. + target.tabIndex !== -1 + ) { + event.preventDefault(); + } + return cb(event, target); + } + let initialClickTarget = (0, import_react10.useRef)(null); + useDocumentEvent( + "mousedown", + (event) => { + var _a3, _b; + if (enabledRef.current) { + initialClickTarget.current = ((_b = (_a3 = event.composedPath) == null ? void 0 : _a3.call(event)) == null ? void 0 : _b[0]) || event.target; + } + }, + true + ); + useDocumentEvent( + "click", + (event) => { + if (!initialClickTarget.current) { + return; + } + handleOutsideClick(event, () => { + return initialClickTarget.current; + }); + initialClickTarget.current = null; + }, + // We will use the `capture` phase so that layers in between with `event.stopPropagation()` + // don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu` + // is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However, + // the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this. + true + ); + useWindowEvent( + "blur", + (event) => handleOutsideClick( + event, + () => window.document.activeElement instanceof HTMLIFrameElement ? window.document.activeElement : null + ), + true + ); +} + +// src/hooks/use-resolve-button-type.ts +var import_react11 = __webpack_require__(/*! react */ "react"); +function resolveType(props) { + var _a3; + if (props.type) + return props.type; + let tag = (_a3 = props.as) != null ? _a3 : "button"; + if (typeof tag === "string" && tag.toLowerCase() === "button") + return "button"; + return void 0; +} +function useResolveButtonType(props, ref) { + let [type, setType] = (0, import_react11.useState)(() => resolveType(props)); + useIsoMorphicEffect(() => { + setType(resolveType(props)); + }, [props.type, props.as]); + useIsoMorphicEffect(() => { + if (type) + return; + if (!ref.current) + return; + if (ref.current instanceof HTMLButtonElement && !ref.current.hasAttribute("type")) { + setType("button"); + } + }, [type, ref]); + return type; +} + +// src/hooks/use-sync-refs.ts +var import_react12 = __webpack_require__(/*! react */ "react"); +var Optional = Symbol(); +function optionalRef(cb, isOptional = true) { + return Object.assign(cb, { [Optional]: isOptional }); +} +function useSyncRefs(...refs) { + let cache = (0, import_react12.useRef)(refs); + (0, import_react12.useEffect)(() => { + cache.current = refs; + }, [refs]); + let syncRefs = useEvent((value) => { + for (let ref of cache.current) { + if (ref == null) + continue; + if (typeof ref === "function") + ref(value); + else + ref.current = value; + } + }); + return refs.every( + (ref) => ref == null || // @ts-expect-error + (ref == null ? void 0 : ref[Optional]) + ) ? void 0 : syncRefs; +} + +// src/hooks/use-tree-walker.ts +var import_react13 = __webpack_require__(/*! react */ "react"); +function useTreeWalker({ + container, + accept, + walk, + enabled = true +}) { + let acceptRef = (0, import_react13.useRef)(accept); + let walkRef = (0, import_react13.useRef)(walk); + (0, import_react13.useEffect)(() => { + acceptRef.current = accept; + walkRef.current = walk; + }, [accept, walk]); + useIsoMorphicEffect(() => { + if (!container) + return; + if (!enabled) + return; + let ownerDocument = getOwnerDocument(container); + if (!ownerDocument) + return; + let accept2 = acceptRef.current; + let walk2 = walkRef.current; + let acceptNode = Object.assign((node) => accept2(node), { acceptNode: accept2 }); + let walker = ownerDocument.createTreeWalker( + container, + NodeFilter.SHOW_ELEMENT, + acceptNode, + // @ts-expect-error This `false` is a simple small fix for older browsers + false + ); + while (walker.nextNode()) + walk2(walker.currentNode); + }, [container, enabled, acceptRef, walkRef]); +} + +// src/utils/calculate-active-index.ts +function assertNever(x) { + throw new Error("Unexpected object: " + x); +} +function calculateActiveIndex(action, resolvers) { + let items = resolvers.resolveItems(); + if (items.length <= 0) + return null; + let currentActiveIndex = resolvers.resolveActiveIndex(); + let activeIndex = currentActiveIndex != null ? currentActiveIndex : -1; + let nextActiveIndex = (() => { + switch (action.focus) { + case 0 /* First */: + return items.findIndex((item) => !resolvers.resolveDisabled(item)); + case 1 /* Previous */: { + let idx = items.slice().reverse().findIndex((item, idx2, all) => { + if (activeIndex !== -1 && all.length - idx2 - 1 >= activeIndex) + return false; + return !resolvers.resolveDisabled(item); + }); + if (idx === -1) + return idx; + return items.length - 1 - idx; + } + case 2 /* Next */: + return items.findIndex((item, idx) => { + if (idx <= activeIndex) + return false; + return !resolvers.resolveDisabled(item); + }); + case 3 /* Last */: { + let idx = items.slice().reverse().findIndex((item) => !resolvers.resolveDisabled(item)); + if (idx === -1) + return idx; + return items.length - 1 - idx; + } + case 4 /* Specific */: + return items.findIndex((item) => resolvers.resolveId(item) === action.id); + case 5 /* Nothing */: + return null; + default: + assertNever(action); + } + })(); + return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex; +} + +// src/utils/render.ts +var import_react14 = __webpack_require__(/*! react */ "react"); + +// src/utils/class-names.ts +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +// src/utils/render.ts +function render({ + ourProps, + theirProps, + slot, + defaultTag, + features, + visible = true, + name +}) { + let props = mergeProps(theirProps, ourProps); + if (visible) + return _render(props, slot, defaultTag, name); + let featureFlags = features != null ? features : 0 /* None */; + if (featureFlags & 2 /* Static */) { + let { static: isStatic = false, ...rest } = props; + if (isStatic) + return _render(rest, slot, defaultTag, name); + } + if (featureFlags & 1 /* RenderStrategy */) { + let { unmount = true, ...rest } = props; + let strategy = unmount ? 0 /* Unmount */ : 1 /* Hidden */; + return match(strategy, { + [0 /* Unmount */]() { + return null; + }, + [1 /* Hidden */]() { + return _render( + { ...rest, ...{ hidden: true, style: { display: "none" } } }, + slot, + defaultTag, + name + ); + } + }); + } + return _render(props, slot, defaultTag, name); +} +function _render(props, slot = {}, tag, name) { + let { + as: Component = tag, + children, + refName = "ref", + ...rest + } = omit(props, ["unmount", "static"]); + let refRelatedProps = props.ref !== void 0 ? { [refName]: props.ref } : {}; + let resolvedChildren = typeof children === "function" ? children(slot) : children; + if ("className" in rest && rest.className && typeof rest.className === "function") { + rest.className = rest.className(slot); + } + let dataAttributes = {}; + if (slot) { + let exposeState = false; + let states = []; + for (let [k, v] of Object.entries(slot)) { + if (typeof v === "boolean") { + exposeState = true; + } + if (v === true) { + states.push(k); + } + } + if (exposeState) + dataAttributes[`data-headlessui-state`] = states.join(" "); + } + if (Component === import_react14.Fragment) { + if (Object.keys(compact(rest)).length > 0) { + if (!(0, import_react14.isValidElement)(resolvedChildren) || Array.isArray(resolvedChildren) && resolvedChildren.length > 1) { + throw new Error( + [ + 'Passing props on "Fragment"!', + "", + `The current component <${name} /> is rendering a "Fragment".`, + `However we need to passthrough the following props:`, + Object.keys(rest).map((line) => ` - ${line}`).join("\n"), + "", + "You can apply a few solutions:", + [ + 'Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".', + "Render a single element as the child so that we can forward the props onto that element." + ].map((line) => ` - ${line}`).join("\n") + ].join("\n") + ); + } + let childProps = resolvedChildren.props; + let newClassName = typeof (childProps == null ? void 0 : childProps.className) === "function" ? (...args) => classNames(childProps == null ? void 0 : childProps.className(...args), rest.className) : classNames(childProps == null ? void 0 : childProps.className, rest.className); + let classNameProps = newClassName ? { className: newClassName } : {}; + return (0, import_react14.cloneElement)( + resolvedChildren, + Object.assign( + {}, + // Filter out undefined values so that they don't override the existing values + mergeProps(resolvedChildren.props, compact(omit(rest, ["ref"]))), + dataAttributes, + refRelatedProps, + mergeRefs(resolvedChildren.ref, refRelatedProps.ref), + classNameProps + ) + ); + } + } + return (0, import_react14.createElement)( + Component, + Object.assign( + {}, + omit(rest, ["ref"]), + Component !== import_react14.Fragment && refRelatedProps, + Component !== import_react14.Fragment && dataAttributes + ), + resolvedChildren + ); +} +function mergeRefs(...refs) { + return { + ref: refs.every((ref) => ref == null) ? void 0 : (value) => { + for (let ref of refs) { + if (ref == null) + continue; + if (typeof ref === "function") + ref(value); + else + ref.current = value; + } + } + }; +} +function mergeProps(...listOfProps) { + var _a3; + if (listOfProps.length === 0) + return {}; + if (listOfProps.length === 1) + return listOfProps[0]; + let target = {}; + let eventHandlers = {}; + for (let props of listOfProps) { + for (let prop in props) { + if (prop.startsWith("on") && typeof props[prop] === "function") { + (_a3 = eventHandlers[prop]) != null ? _a3 : eventHandlers[prop] = []; + eventHandlers[prop].push(props[prop]); + } else { + target[prop] = props[prop]; + } + } + } + if (target.disabled || target["aria-disabled"]) { + return Object.assign( + target, + // Set all event listeners that we collected to `undefined`. This is + // important because of the `cloneElement` from above, which merges the + // existing and new props, they don't just override therefore we have to + // explicitly nullify them. + Object.fromEntries(Object.keys(eventHandlers).map((eventName) => [eventName, void 0])) + ); + } + for (let eventName in eventHandlers) { + Object.assign(target, { + [eventName](event, ...args) { + let handlers = eventHandlers[eventName]; + for (let handler of handlers) { + if ((event instanceof Event || (event == null ? void 0 : event.nativeEvent) instanceof Event) && event.defaultPrevented) { + return; + } + handler(event, ...args); + } + } + }); + } + return target; +} +function forwardRefWithAs(component) { + var _a3; + return Object.assign((0, import_react14.forwardRef)(component), { + displayName: (_a3 = component.displayName) != null ? _a3 : component.name + }); +} +function compact(object) { + let clone = Object.assign({}, object); + for (let key in clone) { + if (clone[key] === void 0) + delete clone[key]; + } + return clone; +} +function omit(object, keysToOmit = []) { + let clone = Object.assign({}, object); + for (let key of keysToOmit) { + if (key in clone) + delete clone[key]; + } + return clone; +} + +// src/utils/bugs.ts +function isDisabledReactIssue7711(element) { + let parent = element.parentElement; + let legend = null; + while (parent && !(parent instanceof HTMLFieldSetElement)) { + if (parent instanceof HTMLLegendElement) + legend = parent; + parent = parent.parentElement; + } + let isParentDisabled = (parent == null ? void 0 : parent.getAttribute("disabled")) === ""; + if (isParentDisabled && isFirstLegend(legend)) + return false; + return isParentDisabled; +} +function isFirstLegend(element) { + if (!element) + return false; + let previous = element.previousElementSibling; + while (previous !== null) { + if (previous instanceof HTMLLegendElement) + return false; + previous = previous.previousElementSibling; + } + return true; +} + +// src/utils/form.ts +function objectToFormEntries(source = {}, parentKey = null, entries = []) { + for (let [key, value] of Object.entries(source)) { + append(entries, composeKey(parentKey, key), value); + } + return entries; +} +function composeKey(parent, key) { + return parent ? parent + "[" + key + "]" : key; +} +function append(entries, key, value) { + if (Array.isArray(value)) { + for (let [subkey, subvalue] of value.entries()) { + append(entries, composeKey(key, subkey.toString()), subvalue); + } + } else if (value instanceof Date) { + entries.push([key, value.toISOString()]); + } else if (typeof value === "boolean") { + entries.push([key, value ? "1" : "0"]); + } else if (typeof value === "string") { + entries.push([key, value]); + } else if (typeof value === "number") { + entries.push([key, `${value}`]); + } else if (value === null || value === void 0) { + entries.push([key, ""]); + } else { + objectToFormEntries(value, key, entries); + } +} +function attemptSubmit(element) { + var _a3; + let form = (_a3 = element == null ? void 0 : element.form) != null ? _a3 : element.closest("form"); + if (!form) + return; + for (let element2 of form.elements) { + if (element2.tagName === "INPUT" && element2.type === "submit" || element2.tagName === "BUTTON" && element2.type === "submit" || element2.nodeName === "INPUT" && element2.type === "image") { + element2.click(); + return; + } + } +} + +// src/internal/hidden.tsx +var DEFAULT_VISUALLY_HIDDEN_TAG = "div"; +function VisuallyHidden(props, ref) { + let { features = 1 /* None */, ...theirProps } = props; + let ourProps = { + ref, + "aria-hidden": (features & 2 /* Focusable */) === 2 /* Focusable */ ? true : void 0, + style: { + position: "fixed", + top: 1, + left: 1, + width: 1, + height: 0, + padding: 0, + margin: -1, + overflow: "hidden", + clip: "rect(0, 0, 0, 0)", + whiteSpace: "nowrap", + borderWidth: "0", + ...(features & 4 /* Hidden */) === 4 /* Hidden */ && !((features & 2 /* Focusable */) === 2 /* Focusable */) && { display: "none" } + } + }; + return render({ + ourProps, + theirProps, + slot: {}, + defaultTag: DEFAULT_VISUALLY_HIDDEN_TAG, + name: "Hidden" + }); +} +var Hidden = forwardRefWithAs(VisuallyHidden); + +// src/internal/open-closed.tsx +var import_react15 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var Context = (0, import_react15.createContext)(null); +Context.displayName = "OpenClosedContext"; +function useOpenClosed() { + return (0, import_react15.useContext)(Context); +} +function OpenClosedProvider({ value, children }) { + return /* @__PURE__ */ import_react15.default.createElement(Context.Provider, { value }, children); +} + +// src/hooks/use-controllable.ts +var import_react16 = __webpack_require__(/*! react */ "react"); +function useControllable(controlledValue, onChange, defaultValue) { + let [internalValue, setInternalValue] = (0, import_react16.useState)(defaultValue); + let isControlled = controlledValue !== void 0; + let wasControlled = (0, import_react16.useRef)(isControlled); + let didWarnOnUncontrolledToControlled = (0, import_react16.useRef)(false); + let didWarnOnControlledToUncontrolled = (0, import_react16.useRef)(false); + if (isControlled && !wasControlled.current && !didWarnOnUncontrolledToControlled.current) { + didWarnOnUncontrolledToControlled.current = true; + wasControlled.current = isControlled; + console.error( + "A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen." + ); + } else if (!isControlled && wasControlled.current && !didWarnOnControlledToUncontrolled.current) { + didWarnOnControlledToUncontrolled.current = true; + wasControlled.current = isControlled; + console.error( + "A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen." + ); + } + return [ + isControlled ? controlledValue : internalValue, + useEvent((value) => { + if (isControlled) { + return onChange == null ? void 0 : onChange(value); + } else { + setInternalValue(value); + return onChange == null ? void 0 : onChange(value); + } + }) + ]; +} + +// src/hooks/use-watch.ts +var import_react17 = __webpack_require__(/*! react */ "react"); +function useWatch(cb, dependencies) { + let track = (0, import_react17.useRef)([]); + let action = useEvent(cb); + (0, import_react17.useEffect)(() => { + let oldValues = [...track.current]; + for (let [idx, value] of dependencies.entries()) { + if (track.current[idx] !== value) { + let returnValue = action(dependencies, oldValues); + track.current = dependencies; + return returnValue; + } + } + }, [action, ...dependencies]); +} + +// src/hooks/use-tracked-pointer.ts +var import_react18 = __webpack_require__(/*! react */ "react"); +function eventToPosition(evt) { + return [evt.screenX, evt.screenY]; +} +function useTrackedPointer() { + let lastPos = (0, import_react18.useRef)([-1, -1]); + return { + wasMoved(evt) { + if (false) {} + let newPos = eventToPosition(evt); + if (lastPos.current[0] === newPos[0] && lastPos.current[1] === newPos[1]) { + return false; + } + lastPos.current = newPos; + return true; + }, + update(evt) { + lastPos.current = eventToPosition(evt); + } + }; +} + +// src/utils/platform.ts +function isIOS() { + return ( + // Check if it is an iPhone + /iPhone/gi.test(window.navigator.platform) || // Check if it is an iPad. iPad reports itself as "MacIntel", but we can check if it is a touch + // screen. Let's hope that Apple doesn't release a touch screen Mac (or maybe this would then + // work as expected 🤔). + /Mac/gi.test(window.navigator.platform) && window.navigator.maxTouchPoints > 0 + ); +} +function isAndroid() { + return /Android/gi.test(window.navigator.userAgent); +} +function isMobile() { + return isIOS() || isAndroid(); +} + +// src/components/combobox/combobox.tsx +function adjustOrderedState(state, adjustment = (i) => i) { + let currentActiveOption = state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null; + let sortedOptions = sortByDomNode( + adjustment(state.options.slice()), + (option) => option.dataRef.current.domRef.current + ); + let adjustedActiveOptionIndex = currentActiveOption ? sortedOptions.indexOf(currentActiveOption) : null; + if (adjustedActiveOptionIndex === -1) { + adjustedActiveOptionIndex = null; + } + return { + options: sortedOptions, + activeOptionIndex: adjustedActiveOptionIndex + }; +} +var reducers = { + [1 /* CloseCombobox */](state) { + var _a3; + if ((_a3 = state.dataRef.current) == null ? void 0 : _a3.disabled) + return state; + if (state.comboboxState === 1 /* Closed */) + return state; + return { ...state, activeOptionIndex: null, comboboxState: 1 /* Closed */ }; + }, + [0 /* OpenCombobox */](state) { + var _a3; + if ((_a3 = state.dataRef.current) == null ? void 0 : _a3.disabled) + return state; + if (state.comboboxState === 0 /* Open */) + return state; + let activeOptionIndex = state.activeOptionIndex; + if (state.dataRef.current) { + let { isSelected } = state.dataRef.current; + let optionIdx = state.options.findIndex((option) => isSelected(option.dataRef.current.value)); + if (optionIdx !== -1) { + activeOptionIndex = optionIdx; + } + } + return { ...state, comboboxState: 0 /* Open */, activeOptionIndex }; + }, + [2 /* GoToOption */](state, action) { + var _a3, _b, _c, _d; + if ((_a3 = state.dataRef.current) == null ? void 0 : _a3.disabled) + return state; + if (((_b = state.dataRef.current) == null ? void 0 : _b.optionsRef.current) && !((_c = state.dataRef.current) == null ? void 0 : _c.optionsPropsRef.current.static) && state.comboboxState === 1 /* Closed */) { + return state; + } + let adjustedState = adjustOrderedState(state); + if (adjustedState.activeOptionIndex === null) { + let localActiveOptionIndex = adjustedState.options.findIndex( + (option) => !option.dataRef.current.disabled + ); + if (localActiveOptionIndex !== -1) { + adjustedState.activeOptionIndex = localActiveOptionIndex; + } + } + let activeOptionIndex = calculateActiveIndex(action, { + resolveItems: () => adjustedState.options, + resolveActiveIndex: () => adjustedState.activeOptionIndex, + resolveId: (item) => item.id, + resolveDisabled: (item) => item.dataRef.current.disabled + }); + return { + ...state, + ...adjustedState, + activeOptionIndex, + activationTrigger: (_d = action.trigger) != null ? _d : 1 /* Other */ + }; + }, + [3 /* RegisterOption */]: (state, action) => { + var _a3, _b; + let option = { id: action.id, dataRef: action.dataRef }; + let adjustedState = adjustOrderedState(state, (options) => [...options, option]); + if (state.activeOptionIndex === null) { + if ((_a3 = state.dataRef.current) == null ? void 0 : _a3.isSelected(action.dataRef.current.value)) { + adjustedState.activeOptionIndex = adjustedState.options.indexOf(option); + } + } + let nextState = { + ...state, + ...adjustedState, + activationTrigger: 1 /* Other */ + }; + if (((_b = state.dataRef.current) == null ? void 0 : _b.__demoMode) && state.dataRef.current.value === void 0) { + nextState.activeOptionIndex = 0; + } + return nextState; + }, + [4 /* UnregisterOption */]: (state, action) => { + let adjustedState = adjustOrderedState(state, (options) => { + let idx = options.findIndex((a) => a.id === action.id); + if (idx !== -1) + options.splice(idx, 1); + return options; + }); + return { + ...state, + ...adjustedState, + activationTrigger: 1 /* Other */ + }; + }, + [5 /* RegisterLabel */]: (state, action) => { + return { + ...state, + labelId: action.id + }; + } +}; +var ComboboxActionsContext = (0, import_react19.createContext)(null); +ComboboxActionsContext.displayName = "ComboboxActionsContext"; +function useActions(component) { + let context = (0, import_react19.useContext)(ComboboxActionsContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useActions); + throw err; + } + return context; +} +var ComboboxDataContext = (0, import_react19.createContext)(null); +ComboboxDataContext.displayName = "ComboboxDataContext"; +function useData(component) { + let context = (0, import_react19.useContext)(ComboboxDataContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useData); + throw err; + } + return context; +} +function stateReducer(state, action) { + return match(action.type, reducers, state, action); +} +var DEFAULT_COMBOBOX_TAG = import_react19.Fragment; +function ComboboxFn(props, ref) { + let { + value: controlledValue, + defaultValue, + onChange: controlledOnChange, + form: formName, + name, + by = (a, z) => a === z, + disabled = false, + __demoMode = false, + nullable = false, + multiple = false, + ...theirProps + } = props; + let [value = multiple ? [] : void 0, theirOnChange] = useControllable( + controlledValue, + controlledOnChange, + defaultValue + ); + let [state, dispatch] = (0, import_react19.useReducer)(stateReducer, { + dataRef: (0, import_react19.createRef)(), + comboboxState: __demoMode ? 0 /* Open */ : 1 /* Closed */, + options: [], + activeOptionIndex: null, + activationTrigger: 1 /* Other */, + labelId: null + }); + let defaultToFirstOption = (0, import_react19.useRef)(false); + let optionsPropsRef = (0, import_react19.useRef)({ static: false, hold: false }); + let labelRef = (0, import_react19.useRef)(null); + let inputRef = (0, import_react19.useRef)(null); + let buttonRef = (0, import_react19.useRef)(null); + let optionsRef = (0, import_react19.useRef)(null); + let compare = useEvent( + // @ts-expect-error Eventually we'll want to tackle this, but for now this will do. + typeof by === "string" ? (a, z) => { + let property = by; + return (a == null ? void 0 : a[property]) === (z == null ? void 0 : z[property]); + } : by + ); + let isSelected = (0, import_react19.useCallback)( + (compareValue) => match(data.mode, { + [1 /* Multi */]: () => value.some((option) => compare(option, compareValue)), + [0 /* Single */]: () => compare(value, compareValue) + }), + [value] + ); + let data = (0, import_react19.useMemo)( + () => ({ + ...state, + optionsPropsRef, + labelRef, + inputRef, + buttonRef, + optionsRef, + value, + defaultValue, + disabled, + mode: multiple ? 1 /* Multi */ : 0 /* Single */, + get activeOptionIndex() { + if (defaultToFirstOption.current && state.activeOptionIndex === null && state.options.length > 0) { + let localActiveOptionIndex = state.options.findIndex( + (option) => !option.dataRef.current.disabled + ); + if (localActiveOptionIndex !== -1) { + return localActiveOptionIndex; + } + } + return state.activeOptionIndex; + }, + compare, + isSelected, + nullable, + __demoMode + }), + [value, defaultValue, disabled, multiple, nullable, __demoMode, state] + ); + let lastActiveOption = (0, import_react19.useRef)( + data.activeOptionIndex !== null ? data.options[data.activeOptionIndex] : null + ); + (0, import_react19.useEffect)(() => { + let currentActiveOption = data.activeOptionIndex !== null ? data.options[data.activeOptionIndex] : null; + if (lastActiveOption.current !== currentActiveOption) { + lastActiveOption.current = currentActiveOption; + } + }); + useIsoMorphicEffect(() => { + state.dataRef.current = data; + }, [data]); + useOutsideClick( + [data.buttonRef, data.inputRef, data.optionsRef], + () => actions.closeCombobox(), + data.comboboxState === 0 /* Open */ + ); + let slot = (0, import_react19.useMemo)( + () => ({ + open: data.comboboxState === 0 /* Open */, + disabled, + activeIndex: data.activeOptionIndex, + activeOption: data.activeOptionIndex === null ? null : data.options[data.activeOptionIndex].dataRef.current.value, + value + }), + [data, disabled, value] + ); + let selectOption = useEvent((id) => { + let option = data.options.find((item) => item.id === id); + if (!option) + return; + onChange(option.dataRef.current.value); + }); + let selectActiveOption = useEvent(() => { + if (data.activeOptionIndex !== null) { + let { dataRef, id } = data.options[data.activeOptionIndex]; + onChange(dataRef.current.value); + actions.goToOption(4 /* Specific */, id); + } + }); + let openCombobox = useEvent(() => { + dispatch({ type: 0 /* OpenCombobox */ }); + defaultToFirstOption.current = true; + }); + let closeCombobox = useEvent(() => { + dispatch({ type: 1 /* CloseCombobox */ }); + defaultToFirstOption.current = false; + }); + let goToOption = useEvent((focus, id, trigger) => { + defaultToFirstOption.current = false; + if (focus === 4 /* Specific */) { + return dispatch({ type: 2 /* GoToOption */, focus: 4 /* Specific */, id, trigger }); + } + return dispatch({ type: 2 /* GoToOption */, focus, trigger }); + }); + let registerOption = useEvent((id, dataRef) => { + dispatch({ type: 3 /* RegisterOption */, id, dataRef }); + return () => { + var _a3; + if (((_a3 = lastActiveOption.current) == null ? void 0 : _a3.id) === id) { + defaultToFirstOption.current = true; + } + dispatch({ type: 4 /* UnregisterOption */, id }); + }; + }); + let registerLabel = useEvent((id) => { + dispatch({ type: 5 /* RegisterLabel */, id }); + return () => dispatch({ type: 5 /* RegisterLabel */, id: null }); + }); + let onChange = useEvent((value2) => { + return match(data.mode, { + [0 /* Single */]() { + return theirOnChange == null ? void 0 : theirOnChange(value2); + }, + [1 /* Multi */]() { + let copy = data.value.slice(); + let idx = copy.findIndex((item) => compare(item, value2)); + if (idx === -1) { + copy.push(value2); + } else { + copy.splice(idx, 1); + } + return theirOnChange == null ? void 0 : theirOnChange(copy); + } + }); + }); + let actions = (0, import_react19.useMemo)( + () => ({ + onChange, + registerOption, + registerLabel, + goToOption, + closeCombobox, + openCombobox, + selectActiveOption, + selectOption + }), + [] + ); + let ourProps = ref === null ? {} : { ref }; + let form = (0, import_react19.useRef)(null); + let d = useDisposables(); + (0, import_react19.useEffect)(() => { + if (!form.current) + return; + if (defaultValue === void 0) + return; + d.addEventListener(form.current, "reset", () => { + onChange(defaultValue); + }); + }, [ + form, + onChange + /* Explicitly ignoring `defaultValue` */ + ]); + return /* @__PURE__ */ import_react19.default.createElement(ComboboxActionsContext.Provider, { value: actions }, /* @__PURE__ */ import_react19.default.createElement(ComboboxDataContext.Provider, { value: data }, /* @__PURE__ */ import_react19.default.createElement( + OpenClosedProvider, + { + value: match(data.comboboxState, { + [0 /* Open */]: 1 /* Open */, + [1 /* Closed */]: 2 /* Closed */ + }) + }, + name != null && value != null && objectToFormEntries({ [name]: value }).map(([name2, value2], idx) => /* @__PURE__ */ import_react19.default.createElement( + Hidden, + { + features: 4 /* Hidden */, + ref: idx === 0 ? (element) => { + var _a3; + form.current = (_a3 = element == null ? void 0 : element.closest("form")) != null ? _a3 : null; + } : void 0, + ...compact({ + key: name2, + as: "input", + type: "hidden", + hidden: true, + readOnly: true, + form: formName, + name: name2, + value: value2 + }) + } + )), + render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_COMBOBOX_TAG, + name: "Combobox" + }) + ))); +} +var DEFAULT_INPUT_TAG = "input"; +function InputFn(props, ref) { + var _a3, _b, _c, _d; + let internalId = useId(); + let { + id = `headlessui-combobox-input-${internalId}`, + onChange, + displayValue, + // @ts-ignore: We know this MAY NOT exist for a given tag but we only care when it _does_ exist. + type = "text", + ...theirProps + } = props; + let data = useData("Combobox.Input"); + let actions = useActions("Combobox.Input"); + let inputRef = useSyncRefs(data.inputRef, ref); + let isTyping = (0, import_react19.useRef)(false); + let d = useDisposables(); + let currentDisplayValue = function() { + var _a4; + if (typeof displayValue === "function" && data.value !== void 0) { + return (_a4 = displayValue(data.value)) != null ? _a4 : ""; + } else if (typeof data.value === "string") { + return data.value; + } else { + return ""; + } + }(); + useWatch( + ([currentDisplayValue2, state], [oldCurrentDisplayValue, oldState]) => { + if (isTyping.current) + return; + if (!data.inputRef.current) + return; + if (oldState === 0 /* Open */ && state === 1 /* Closed */) { + data.inputRef.current.value = currentDisplayValue2; + } else if (currentDisplayValue2 !== oldCurrentDisplayValue) { + data.inputRef.current.value = currentDisplayValue2; + } + }, + [currentDisplayValue, data.comboboxState] + ); + useWatch( + ([newState], [oldState]) => { + if (newState === 0 /* Open */ && oldState === 1 /* Closed */) { + let input = data.inputRef.current; + if (!input) + return; + let currentValue = input.value; + let { selectionStart, selectionEnd, selectionDirection } = input; + input.value = ""; + input.value = currentValue; + if (selectionDirection !== null) { + input.setSelectionRange(selectionStart, selectionEnd, selectionDirection); + } else { + input.setSelectionRange(selectionStart, selectionEnd); + } + } + }, + [data.comboboxState] + ); + let isComposing = (0, import_react19.useRef)(false); + let composedChangeEvent = (0, import_react19.useRef)(null); + let handleCompositionStart = useEvent(() => { + isComposing.current = true; + }); + let handleCompositionEnd = useEvent(() => { + d.nextFrame(() => { + isComposing.current = false; + if (composedChangeEvent.current) { + actions.openCombobox(); + onChange == null ? void 0 : onChange(composedChangeEvent.current); + composedChangeEvent.current = null; + } + }); + }); + let handleKeyDown = useEvent((event) => { + isTyping.current = true; + switch (event.key) { + case "Backspace" /* Backspace */: + case "Delete" /* Delete */: + if (data.mode !== 0 /* Single */) + return; + if (!data.nullable) + return; + let input = event.currentTarget; + d.requestAnimationFrame(() => { + if (input.value === "") { + actions.onChange(null); + if (data.optionsRef.current) { + data.optionsRef.current.scrollTop = 0; + } + actions.goToOption(5 /* Nothing */); + } + }); + break; + case "Enter" /* Enter */: + isTyping.current = false; + if (data.comboboxState !== 0 /* Open */) + return; + if (isComposing.current) + return; + event.preventDefault(); + event.stopPropagation(); + if (data.activeOptionIndex === null) { + actions.closeCombobox(); + return; + } + actions.selectActiveOption(); + if (data.mode === 0 /* Single */) { + actions.closeCombobox(); + } + break; + case "ArrowDown" /* ArrowDown */: + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return match(data.comboboxState, { + [0 /* Open */]: () => { + actions.goToOption(2 /* Next */); + }, + [1 /* Closed */]: () => { + actions.openCombobox(); + } + }); + case "ArrowUp" /* ArrowUp */: + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return match(data.comboboxState, { + [0 /* Open */]: () => { + actions.goToOption(1 /* Previous */); + }, + [1 /* Closed */]: () => { + actions.openCombobox(); + d.nextFrame(() => { + if (!data.value) { + actions.goToOption(3 /* Last */); + } + }); + } + }); + case "Home" /* Home */: + if (event.shiftKey) { + break; + } + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(0 /* First */); + case "PageUp" /* PageUp */: + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(0 /* First */); + case "End" /* End */: + if (event.shiftKey) { + break; + } + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(3 /* Last */); + case "PageDown" /* PageDown */: + isTyping.current = false; + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(3 /* Last */); + case "Escape" /* Escape */: + isTyping.current = false; + if (data.comboboxState !== 0 /* Open */) + return; + event.preventDefault(); + if (data.optionsRef.current && !data.optionsPropsRef.current.static) { + event.stopPropagation(); + } + return actions.closeCombobox(); + case "Tab" /* Tab */: + isTyping.current = false; + if (data.comboboxState !== 0 /* Open */) + return; + if (data.mode === 0 /* Single */) + actions.selectActiveOption(); + actions.closeCombobox(); + break; + } + }); + let handleChange = useEvent((event) => { + if (isComposing.current) { + composedChangeEvent.current = event; + return; + } + actions.openCombobox(); + onChange == null ? void 0 : onChange(event); + }); + let handleBlur = useEvent(() => { + isTyping.current = false; + }); + let labelledby = useComputed(() => { + if (!data.labelId) + return void 0; + return [data.labelId].join(" "); + }, [data.labelId]); + let slot = (0, import_react19.useMemo)( + () => ({ open: data.comboboxState === 0 /* Open */, disabled: data.disabled }), + [data] + ); + let ourProps = { + ref: inputRef, + id, + role: "combobox", + type, + "aria-controls": (_a3 = data.optionsRef.current) == null ? void 0 : _a3.id, + "aria-expanded": data.disabled ? void 0 : data.comboboxState === 0 /* Open */, + "aria-activedescendant": data.activeOptionIndex === null ? void 0 : (_b = data.options[data.activeOptionIndex]) == null ? void 0 : _b.id, + "aria-labelledby": labelledby, + "aria-autocomplete": "list", + defaultValue: (_d = (_c = props.defaultValue) != null ? _c : data.defaultValue !== void 0 ? displayValue == null ? void 0 : displayValue(data.defaultValue) : null) != null ? _d : data.defaultValue, + disabled: data.disabled, + onCompositionStart: handleCompositionStart, + onCompositionEnd: handleCompositionEnd, + onKeyDown: handleKeyDown, + onChange: handleChange, + onBlur: handleBlur + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_INPUT_TAG, + name: "Combobox.Input" + }); +} +var DEFAULT_BUTTON_TAG = "button"; +function ButtonFn(props, ref) { + var _a3; + let data = useData("Combobox.Button"); + let actions = useActions("Combobox.Button"); + let buttonRef = useSyncRefs(data.buttonRef, ref); + let internalId = useId(); + let { id = `headlessui-combobox-button-${internalId}`, ...theirProps } = props; + let d = useDisposables(); + let handleKeyDown = useEvent((event) => { + switch (event.key) { + case "ArrowDown" /* ArrowDown */: + event.preventDefault(); + event.stopPropagation(); + if (data.comboboxState === 1 /* Closed */) { + actions.openCombobox(); + } + return d.nextFrame(() => { + var _a4; + return (_a4 = data.inputRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + case "ArrowUp" /* ArrowUp */: + event.preventDefault(); + event.stopPropagation(); + if (data.comboboxState === 1 /* Closed */) { + actions.openCombobox(); + d.nextFrame(() => { + if (!data.value) { + actions.goToOption(3 /* Last */); + } + }); + } + return d.nextFrame(() => { + var _a4; + return (_a4 = data.inputRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + case "Escape" /* Escape */: + if (data.comboboxState !== 0 /* Open */) + return; + event.preventDefault(); + if (data.optionsRef.current && !data.optionsPropsRef.current.static) { + event.stopPropagation(); + } + actions.closeCombobox(); + return d.nextFrame(() => { + var _a4; + return (_a4 = data.inputRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + default: + return; + } + }); + let handleClick = useEvent((event) => { + if (isDisabledReactIssue7711(event.currentTarget)) + return event.preventDefault(); + if (data.comboboxState === 0 /* Open */) { + actions.closeCombobox(); + } else { + event.preventDefault(); + actions.openCombobox(); + } + d.nextFrame(() => { + var _a4; + return (_a4 = data.inputRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + }); + let labelledby = useComputed(() => { + if (!data.labelId) + return void 0; + return [data.labelId, id].join(" "); + }, [data.labelId, id]); + let slot = (0, import_react19.useMemo)( + () => ({ + open: data.comboboxState === 0 /* Open */, + disabled: data.disabled, + value: data.value + }), + [data] + ); + let ourProps = { + ref: buttonRef, + id, + type: useResolveButtonType(props, data.buttonRef), + tabIndex: -1, + "aria-haspopup": "listbox", + "aria-controls": (_a3 = data.optionsRef.current) == null ? void 0 : _a3.id, + "aria-expanded": data.disabled ? void 0 : data.comboboxState === 0 /* Open */, + "aria-labelledby": labelledby, + disabled: data.disabled, + onClick: handleClick, + onKeyDown: handleKeyDown + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG, + name: "Combobox.Button" + }); +} +var DEFAULT_LABEL_TAG = "label"; +function LabelFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-combobox-label-${internalId}`, ...theirProps } = props; + let data = useData("Combobox.Label"); + let actions = useActions("Combobox.Label"); + let labelRef = useSyncRefs(data.labelRef, ref); + useIsoMorphicEffect(() => actions.registerLabel(id), [id]); + let handleClick = useEvent(() => { + var _a3; + return (_a3 = data.inputRef.current) == null ? void 0 : _a3.focus({ preventScroll: true }); + }); + let slot = (0, import_react19.useMemo)( + () => ({ open: data.comboboxState === 0 /* Open */, disabled: data.disabled }), + [data] + ); + let ourProps = { ref: labelRef, id, onClick: handleClick }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_LABEL_TAG, + name: "Combobox.Label" + }); +} +var DEFAULT_OPTIONS_TAG = "ul"; +var OptionsRenderFeatures = 1 /* RenderStrategy */ | 2 /* Static */; +function OptionsFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-combobox-options-${internalId}`, hold = false, ...theirProps } = props; + let data = useData("Combobox.Options"); + let optionsRef = useSyncRefs(data.optionsRef, ref); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return data.comboboxState === 0 /* Open */; + })(); + useIsoMorphicEffect(() => { + var _a3; + data.optionsPropsRef.current.static = (_a3 = props.static) != null ? _a3 : false; + }, [data.optionsPropsRef, props.static]); + useIsoMorphicEffect(() => { + data.optionsPropsRef.current.hold = hold; + }, [data.optionsPropsRef, hold]); + useTreeWalker({ + container: data.optionsRef.current, + enabled: data.comboboxState === 0 /* Open */, + accept(node) { + if (node.getAttribute("role") === "option") + return NodeFilter.FILTER_REJECT; + if (node.hasAttribute("role")) + return NodeFilter.FILTER_SKIP; + return NodeFilter.FILTER_ACCEPT; + }, + walk(node) { + node.setAttribute("role", "none"); + } + }); + let labelledby = useComputed( + () => { + var _a3, _b; + return (_b = data.labelId) != null ? _b : (_a3 = data.buttonRef.current) == null ? void 0 : _a3.id; + }, + [data.labelId, data.buttonRef.current] + ); + let slot = (0, import_react19.useMemo)( + () => ({ open: data.comboboxState === 0 /* Open */ }), + [data] + ); + let ourProps = { + "aria-labelledby": labelledby, + role: "listbox", + "aria-multiselectable": data.mode === 1 /* Multi */ ? true : void 0, + id, + ref: optionsRef + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OPTIONS_TAG, + features: OptionsRenderFeatures, + visible, + name: "Combobox.Options" + }); +} +var DEFAULT_OPTION_TAG = "li"; +function OptionFn(props, ref) { + var _a3, _b; + let internalId = useId(); + let { + id = `headlessui-combobox-option-${internalId}`, + disabled = false, + value, + ...theirProps + } = props; + let data = useData("Combobox.Option"); + let actions = useActions("Combobox.Option"); + let active = data.activeOptionIndex !== null ? data.options[data.activeOptionIndex].id === id : false; + let selected = data.isSelected(value); + let internalOptionRef = (0, import_react19.useRef)(null); + let bag = useLatestValue({ + disabled, + value, + domRef: internalOptionRef, + textValue: (_b = (_a3 = internalOptionRef.current) == null ? void 0 : _a3.textContent) == null ? void 0 : _b.toLowerCase() + }); + let optionRef = useSyncRefs(ref, internalOptionRef); + let select = useEvent(() => actions.selectOption(id)); + useIsoMorphicEffect(() => actions.registerOption(id, bag), [bag, id]); + let enableScrollIntoView = (0, import_react19.useRef)(data.__demoMode ? false : true); + useIsoMorphicEffect(() => { + if (!data.__demoMode) + return; + let d = disposables(); + d.requestAnimationFrame(() => { + enableScrollIntoView.current = true; + }); + return d.dispose; + }, []); + useIsoMorphicEffect(() => { + if (data.comboboxState !== 0 /* Open */) + return; + if (!active) + return; + if (!enableScrollIntoView.current) + return; + if (data.activationTrigger === 0 /* Pointer */) + return; + let d = disposables(); + d.requestAnimationFrame(() => { + var _a4, _b2; + (_b2 = (_a4 = internalOptionRef.current) == null ? void 0 : _a4.scrollIntoView) == null ? void 0 : _b2.call(_a4, { block: "nearest" }); + }); + return d.dispose; + }, [ + internalOptionRef, + active, + data.comboboxState, + data.activationTrigger, + /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ + data.activeOptionIndex + ]); + let handleClick = useEvent((event) => { + if (disabled) + return event.preventDefault(); + select(); + if (data.mode === 0 /* Single */) { + actions.closeCombobox(); + } + if (!isMobile()) { + requestAnimationFrame(() => { + var _a4; + return (_a4 = data.inputRef.current) == null ? void 0 : _a4.focus(); + }); + } + }); + let handleFocus = useEvent(() => { + if (disabled) + return actions.goToOption(5 /* Nothing */); + actions.goToOption(4 /* Specific */, id); + }); + let pointer = useTrackedPointer(); + let handleEnter = useEvent((evt) => pointer.update(evt)); + let handleMove = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (active) + return; + actions.goToOption(4 /* Specific */, id, 0 /* Pointer */); + }); + let handleLeave = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (!active) + return; + if (data.optionsPropsRef.current.hold) + return; + actions.goToOption(5 /* Nothing */); + }); + let slot = (0, import_react19.useMemo)( + () => ({ active, selected, disabled }), + [active, selected, disabled] + ); + let ourProps = { + id, + ref: optionRef, + role: "option", + tabIndex: disabled === true ? void 0 : -1, + "aria-disabled": disabled === true ? true : void 0, + // According to the WAI-ARIA best practices, we should use aria-checked for + // multi-select,but Voice-Over disagrees. So we use aria-checked instead for + // both single and multi-select. + "aria-selected": selected, + disabled: void 0, + // Never forward the `disabled` prop + onClick: handleClick, + onFocus: handleFocus, + onPointerEnter: handleEnter, + onMouseEnter: handleEnter, + onPointerMove: handleMove, + onMouseMove: handleMove, + onPointerLeave: handleLeave, + onMouseLeave: handleLeave + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OPTION_TAG, + name: "Combobox.Option" + }); +} +var ComboboxRoot = forwardRefWithAs(ComboboxFn); +var Button = forwardRefWithAs(ButtonFn); +var Input = forwardRefWithAs(InputFn); +var Label = forwardRefWithAs(LabelFn); +var Options = forwardRefWithAs(OptionsFn); +var Option = forwardRefWithAs(OptionFn); +var Combobox = Object.assign(ComboboxRoot, { Input, Button, Label, Options, Option }); + +// src/components/dialog/dialog.tsx +var import_react31 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/components/focus-trap/focus-trap.tsx +var import_react25 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/hooks/use-tab-direction.ts +var import_react20 = __webpack_require__(/*! react */ "react"); +function useTabDirection() { + let direction = (0, import_react20.useRef)(0 /* Forwards */); + useWindowEvent( + "keydown", + (event) => { + if (event.key === "Tab") { + direction.current = event.shiftKey ? 1 /* Backwards */ : 0 /* Forwards */; + } + }, + true + ); + return direction; +} + +// src/hooks/use-is-mounted.ts +var import_react21 = __webpack_require__(/*! react */ "react"); +function useIsMounted() { + let mounted = (0, import_react21.useRef)(false); + useIsoMorphicEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); + return mounted; +} + +// src/hooks/use-owner.ts +var import_react22 = __webpack_require__(/*! react */ "react"); +function useOwnerDocument(...args) { + return (0, import_react22.useMemo)(() => getOwnerDocument(...args), [...args]); +} + +// src/hooks/use-event-listener.ts +var import_react23 = __webpack_require__(/*! react */ "react"); +function useEventListener(element, type, listener, options) { + let listenerRef = useLatestValue(listener); + (0, import_react23.useEffect)(() => { + element = element != null ? element : window; + function handler(event) { + listenerRef.current(event); + } + element.addEventListener(type, handler, options); + return () => element.removeEventListener(type, handler, options); + }, [element, type, options]); +} + +// src/utils/document-ready.ts +function onDocumentReady(cb) { + function check() { + if (document.readyState === "loading") + return; + cb(); + document.removeEventListener("DOMContentLoaded", check); + } + if (typeof window !== "undefined" && typeof document !== "undefined") { + document.addEventListener("DOMContentLoaded", check); + check(); + } +} + +// src/hooks/use-on-unmount.ts +var import_react24 = __webpack_require__(/*! react */ "react"); +function useOnUnmount(cb) { + let stableCb = useEvent(cb); + let trulyUnmounted = (0, import_react24.useRef)(false); + (0, import_react24.useEffect)(() => { + trulyUnmounted.current = false; + return () => { + trulyUnmounted.current = true; + microTask(() => { + if (!trulyUnmounted.current) + return; + stableCb(); + }); + }; + }, [stableCb]); +} + +// src/components/focus-trap/focus-trap.tsx +function resolveContainers(containers) { + if (!containers) + return /* @__PURE__ */ new Set(); + if (typeof containers === "function") + return new Set(containers()); + let all = /* @__PURE__ */ new Set(); + for (let container of containers.current) { + if (container.current instanceof HTMLElement) { + all.add(container.current); + } + } + return all; +} +var DEFAULT_FOCUS_TRAP_TAG = "div"; +var Features3 = /* @__PURE__ */ ((Features4) => { + Features4[Features4["None"] = 1] = "None"; + Features4[Features4["InitialFocus"] = 2] = "InitialFocus"; + Features4[Features4["TabLock"] = 4] = "TabLock"; + Features4[Features4["FocusLock"] = 8] = "FocusLock"; + Features4[Features4["RestoreFocus"] = 16] = "RestoreFocus"; + Features4[Features4["All"] = 30] = "All"; + return Features4; +})(Features3 || {}); +function FocusTrapFn(props, ref) { + let container = (0, import_react25.useRef)(null); + let focusTrapRef = useSyncRefs(container, ref); + let { initialFocus, containers, features = 30 /* All */, ...theirProps } = props; + if (!useServerHandoffComplete()) { + features = 1 /* None */; + } + let ownerDocument = useOwnerDocument(container); + useRestoreFocus({ ownerDocument }, Boolean(features & 16 /* RestoreFocus */)); + let previousActiveElement = useInitialFocus( + { ownerDocument, container, initialFocus }, + Boolean(features & 2 /* InitialFocus */) + ); + useFocusLock( + { ownerDocument, container, containers, previousActiveElement }, + Boolean(features & 8 /* FocusLock */) + ); + let direction = useTabDirection(); + let handleFocus = useEvent((e) => { + let el = container.current; + if (!el) + return; + let wrapper = false ? 0 : (cb) => cb(); + wrapper(() => { + match(direction.current, { + [0 /* Forwards */]: () => { + focusIn(el, 1 /* First */, { skipElements: [e.relatedTarget] }); + }, + [1 /* Backwards */]: () => { + focusIn(el, 8 /* Last */, { skipElements: [e.relatedTarget] }); + } + }); + }); + }); + let d = useDisposables(); + let recentlyUsedTabKey = (0, import_react25.useRef)(false); + let ourProps = { + ref: focusTrapRef, + onKeyDown(e) { + if (e.key == "Tab") { + recentlyUsedTabKey.current = true; + d.requestAnimationFrame(() => { + recentlyUsedTabKey.current = false; + }); + } + }, + onBlur(e) { + let allContainers = resolveContainers(containers); + if (container.current instanceof HTMLElement) + allContainers.add(container.current); + let relatedTarget = e.relatedTarget; + if (!(relatedTarget instanceof HTMLElement)) + return; + if (relatedTarget.dataset.headlessuiFocusGuard === "true") { + return; + } + if (!contains(allContainers, relatedTarget)) { + if (recentlyUsedTabKey.current) { + focusIn( + container.current, + match(direction.current, { + [0 /* Forwards */]: () => 4 /* Next */, + [1 /* Backwards */]: () => 2 /* Previous */ + }) | 16 /* WrapAround */, + { relativeTo: e.target } + ); + } else if (e.target instanceof HTMLElement) { + focusElement(e.target); + } + } + } + }; + return /* @__PURE__ */ import_react25.default.createElement(import_react25.default.Fragment, null, Boolean(features & 4 /* TabLock */) && /* @__PURE__ */ import_react25.default.createElement( + Hidden, + { + as: "button", + type: "button", + "data-headlessui-focus-guard": true, + onFocus: handleFocus, + features: 2 /* Focusable */ + } + ), render({ + ourProps, + theirProps, + defaultTag: DEFAULT_FOCUS_TRAP_TAG, + name: "FocusTrap" + }), Boolean(features & 4 /* TabLock */) && /* @__PURE__ */ import_react25.default.createElement( + Hidden, + { + as: "button", + type: "button", + "data-headlessui-focus-guard": true, + onFocus: handleFocus, + features: 2 /* Focusable */ + } + )); +} +var FocusTrapRoot = forwardRefWithAs(FocusTrapFn); +var FocusTrap = Object.assign(FocusTrapRoot, { + features: Features3 +}); +var history = []; +onDocumentReady(() => { + function handle(e) { + if (!(e.target instanceof HTMLElement)) + return; + if (e.target === document.body) + return; + if (history[0] === e.target) + return; + history.unshift(e.target); + history = history.filter((x) => x != null && x.isConnected); + history.splice(10); + } + window.addEventListener("click", handle, { capture: true }); + window.addEventListener("mousedown", handle, { capture: true }); + window.addEventListener("focus", handle, { capture: true }); + document.body.addEventListener("click", handle, { capture: true }); + document.body.addEventListener("mousedown", handle, { capture: true }); + document.body.addEventListener("focus", handle, { capture: true }); +}); +function useRestoreElement(enabled = true) { + let localHistory = (0, import_react25.useRef)(history.slice()); + useWatch( + ([newEnabled], [oldEnabled]) => { + if (oldEnabled === true && newEnabled === false) { + microTask(() => { + localHistory.current.splice(0); + }); + } + if (oldEnabled === false && newEnabled === true) { + localHistory.current = history.slice(); + } + }, + [enabled, history, localHistory] + ); + return useEvent(() => { + var _a3; + return (_a3 = localHistory.current.find((x) => x != null && x.isConnected)) != null ? _a3 : null; + }); +} +function useRestoreFocus({ ownerDocument }, enabled) { + let getRestoreElement = useRestoreElement(enabled); + useWatch(() => { + if (enabled) + return; + if ((ownerDocument == null ? void 0 : ownerDocument.activeElement) === (ownerDocument == null ? void 0 : ownerDocument.body)) { + focusElement(getRestoreElement()); + } + }, [enabled]); + useOnUnmount(() => { + if (!enabled) + return; + focusElement(getRestoreElement()); + }); +} +function useInitialFocus({ + ownerDocument, + container, + initialFocus +}, enabled) { + let previousActiveElement = (0, import_react25.useRef)(null); + let mounted = useIsMounted(); + useWatch(() => { + if (!enabled) + return; + let containerElement = container.current; + if (!containerElement) + return; + microTask(() => { + if (!mounted.current) { + return; + } + let activeElement = ownerDocument == null ? void 0 : ownerDocument.activeElement; + if (initialFocus == null ? void 0 : initialFocus.current) { + if ((initialFocus == null ? void 0 : initialFocus.current) === activeElement) { + previousActiveElement.current = activeElement; + return; + } + } else if (containerElement.contains(activeElement)) { + previousActiveElement.current = activeElement; + return; + } + if (initialFocus == null ? void 0 : initialFocus.current) { + focusElement(initialFocus.current); + } else { + if (focusIn(containerElement, 1 /* First */) === 0 /* Error */) { + console.warn("There are no focusable elements inside the "); + } + } + previousActiveElement.current = ownerDocument == null ? void 0 : ownerDocument.activeElement; + }); + }, [enabled]); + return previousActiveElement; +} +function useFocusLock({ + ownerDocument, + container, + containers, + previousActiveElement +}, enabled) { + let mounted = useIsMounted(); + useEventListener( + ownerDocument == null ? void 0 : ownerDocument.defaultView, + "focus", + (event) => { + if (!enabled) + return; + if (!mounted.current) + return; + let allContainers = resolveContainers(containers); + if (container.current instanceof HTMLElement) + allContainers.add(container.current); + let previous = previousActiveElement.current; + if (!previous) + return; + let toElement = event.target; + if (toElement && toElement instanceof HTMLElement) { + if (!contains(allContainers, toElement)) { + event.preventDefault(); + event.stopPropagation(); + focusElement(previous); + } else { + previousActiveElement.current = toElement; + focusElement(toElement); + } + } else { + focusElement(previousActiveElement.current); + } + }, + true + ); +} +function contains(containers, element) { + for (let container of containers) { + if (container.contains(element)) + return true; + } + return false; +} + +// src/components/portal/portal.tsx +var import_react27 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var import_react_dom = __webpack_require__(/*! react-dom */ "react-dom"); + +// src/internal/portal-force-root.tsx +var import_react26 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var ForcePortalRootContext = (0, import_react26.createContext)(false); +function usePortalRoot() { + return (0, import_react26.useContext)(ForcePortalRootContext); +} +function ForcePortalRoot(props) { + return /* @__PURE__ */ import_react26.default.createElement(ForcePortalRootContext.Provider, { value: props.force }, props.children); +} + +// src/components/portal/portal.tsx +function usePortalTarget(ref) { + let forceInRoot = usePortalRoot(); + let groupTarget = (0, import_react27.useContext)(PortalGroupContext); + let ownerDocument = useOwnerDocument(ref); + let [target, setTarget] = (0, import_react27.useState)(() => { + if (!forceInRoot && groupTarget !== null) + return null; + if (env.isServer) + return null; + let existingRoot = ownerDocument == null ? void 0 : ownerDocument.getElementById("headlessui-portal-root"); + if (existingRoot) + return existingRoot; + if (ownerDocument === null) + return null; + let root = ownerDocument.createElement("div"); + root.setAttribute("id", "headlessui-portal-root"); + return ownerDocument.body.appendChild(root); + }); + (0, import_react27.useEffect)(() => { + if (target === null) + return; + if (!(ownerDocument == null ? void 0 : ownerDocument.body.contains(target))) { + ownerDocument == null ? void 0 : ownerDocument.body.appendChild(target); + } + }, [target, ownerDocument]); + (0, import_react27.useEffect)(() => { + if (forceInRoot) + return; + if (groupTarget === null) + return; + setTarget(groupTarget.current); + }, [groupTarget, setTarget, forceInRoot]); + return target; +} +var DEFAULT_PORTAL_TAG = import_react27.Fragment; +function PortalFn(props, ref) { + let theirProps = props; + let internalPortalRootRef = (0, import_react27.useRef)(null); + let portalRef = useSyncRefs( + optionalRef((ref2) => { + internalPortalRootRef.current = ref2; + }), + ref + ); + let ownerDocument = useOwnerDocument(internalPortalRootRef); + let target = usePortalTarget(internalPortalRootRef); + let [element] = (0, import_react27.useState)( + () => { + var _a3; + return env.isServer ? null : (_a3 = ownerDocument == null ? void 0 : ownerDocument.createElement("div")) != null ? _a3 : null; + } + ); + let parent = (0, import_react27.useContext)(PortalParentContext); + let ready = useServerHandoffComplete(); + useIsoMorphicEffect(() => { + if (!target || !element) + return; + if (!target.contains(element)) { + element.setAttribute("data-headlessui-portal", ""); + target.appendChild(element); + } + }, [target, element]); + useIsoMorphicEffect(() => { + if (!element) + return; + if (!parent) + return; + return parent.register(element); + }, [parent, element]); + useOnUnmount(() => { + var _a3; + if (!target || !element) + return; + if (element instanceof Node && target.contains(element)) { + target.removeChild(element); + } + if (target.childNodes.length <= 0) { + (_a3 = target.parentElement) == null ? void 0 : _a3.removeChild(target); + } + }); + if (!ready) + return null; + let ourProps = { ref: portalRef }; + return !target || !element ? null : (0, import_react_dom.createPortal)( + render({ + ourProps, + theirProps, + defaultTag: DEFAULT_PORTAL_TAG, + name: "Portal" + }), + element + ); +} +var DEFAULT_GROUP_TAG = import_react27.Fragment; +var PortalGroupContext = (0, import_react27.createContext)(null); +function GroupFn(props, ref) { + let { target, ...theirProps } = props; + let groupRef = useSyncRefs(ref); + let ourProps = { ref: groupRef }; + return /* @__PURE__ */ import_react27.default.createElement(PortalGroupContext.Provider, { value: target }, render({ + ourProps, + theirProps, + defaultTag: DEFAULT_GROUP_TAG, + name: "Popover.Group" + })); +} +var PortalParentContext = (0, import_react27.createContext)(null); +function useNestedPortals() { + let parent = (0, import_react27.useContext)(PortalParentContext); + let portals = (0, import_react27.useRef)([]); + let register = useEvent((portal) => { + portals.current.push(portal); + if (parent) + parent.register(portal); + return () => unregister(portal); + }); + let unregister = useEvent((portal) => { + let idx = portals.current.indexOf(portal); + if (idx !== -1) + portals.current.splice(idx, 1); + if (parent) + parent.unregister(portal); + }); + let api = (0, import_react27.useMemo)( + () => ({ register, unregister, portals }), + [register, unregister, portals] + ); + return [ + portals, + (0, import_react27.useMemo)(() => { + return function PortalWrapper({ children }) { + return /* @__PURE__ */ import_react27.default.createElement(PortalParentContext.Provider, { value: api }, children); + }; + }, [api]) + ]; +} +var PortalRoot = forwardRefWithAs(PortalFn); +var Group = forwardRefWithAs(GroupFn); +var Portal = Object.assign(PortalRoot, { Group }); + +// src/components/description/description.tsx +var import_react28 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var DescriptionContext = (0, import_react28.createContext)(null); +function useDescriptionContext() { + let context = (0, import_react28.useContext)(DescriptionContext); + if (context === null) { + let err = new Error( + "You used a component, but it is not inside a relevant parent." + ); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useDescriptionContext); + throw err; + } + return context; +} +function useDescriptions() { + let [descriptionIds, setDescriptionIds] = (0, import_react28.useState)([]); + return [ + // The actual id's as string or undefined + descriptionIds.length > 0 ? descriptionIds.join(" ") : void 0, + // The provider component + (0, import_react28.useMemo)(() => { + return function DescriptionProvider(props) { + let register = useEvent((value) => { + setDescriptionIds((existing) => [...existing, value]); + return () => setDescriptionIds((existing) => { + let clone = existing.slice(); + let idx = clone.indexOf(value); + if (idx !== -1) + clone.splice(idx, 1); + return clone; + }); + }); + let contextBag = (0, import_react28.useMemo)( + () => ({ register, slot: props.slot, name: props.name, props: props.props }), + [register, props.slot, props.name, props.props] + ); + return /* @__PURE__ */ import_react28.default.createElement(DescriptionContext.Provider, { value: contextBag }, props.children); + }; + }, [setDescriptionIds]) + ]; +} +var DEFAULT_DESCRIPTION_TAG = "p"; +function DescriptionFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-description-${internalId}`, ...theirProps } = props; + let context = useDescriptionContext(); + let descriptionRef = useSyncRefs(ref); + useIsoMorphicEffect(() => context.register(id), [id, context.register]); + let ourProps = { ref: descriptionRef, ...context.props, id }; + return render({ + ourProps, + theirProps, + slot: context.slot || {}, + defaultTag: DEFAULT_DESCRIPTION_TAG, + name: context.name || "Description" + }); +} +var DescriptionRoot = forwardRefWithAs(DescriptionFn); +var Description = Object.assign(DescriptionRoot, { + // +}); + +// src/internal/stack-context.tsx +var import_react29 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var StackContext = (0, import_react29.createContext)(() => { +}); +StackContext.displayName = "StackContext"; +function useStackContext() { + return (0, import_react29.useContext)(StackContext); +} +function StackProvider({ + children, + onUpdate, + type, + element, + enabled +}) { + let parentUpdate = useStackContext(); + let notify = useEvent((...args) => { + onUpdate == null ? void 0 : onUpdate(...args); + parentUpdate(...args); + }); + useIsoMorphicEffect(() => { + let shouldNotify = enabled === void 0 || enabled === true; + shouldNotify && notify(0 /* Add */, type, element); + return () => { + shouldNotify && notify(1 /* Remove */, type, element); + }; + }, [notify, type, element, enabled]); + return /* @__PURE__ */ import_react29.default.createElement(StackContext.Provider, { value: notify }, children); +} + +// src/use-sync-external-store-shim/index.ts +var React11 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts +var React10 = __toESM(__webpack_require__(/*! react */ "react"), 1); +function isPolyfill(x, y) { + return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y; +} +var is = typeof Object.is === "function" ? Object.is : isPolyfill; +var { useState: useState8, useEffect: useEffect14, useLayoutEffect: useLayoutEffect2, useDebugValue } = React10; +var didWarnOld18Alpha = false; +var didWarnUncachedGetSnapshot = false; +function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) { + if (true) { + if (!didWarnOld18Alpha) { + if ("startTransition" in React10) { + didWarnOld18Alpha = true; + console.error( + "You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release." + ); + } + } + } + const value = getSnapshot(); + if (true) { + if (!didWarnUncachedGetSnapshot) { + const cachedValue = getSnapshot(); + if (!is(value, cachedValue)) { + console.error("The result of getSnapshot should be cached to avoid an infinite loop"); + didWarnUncachedGetSnapshot = true; + } + } + } + const [{ inst }, forceUpdate] = useState8({ inst: { value, getSnapshot } }); + useLayoutEffect2(() => { + inst.value = value; + inst.getSnapshot = getSnapshot; + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } + }, [subscribe, value, getSnapshot]); + useEffect14(() => { + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } + const handleStoreChange = () => { + if (checkIfSnapshotChanged(inst)) { + forceUpdate({ inst }); + } + }; + return subscribe(handleStoreChange); + }, [subscribe]); + useDebugValue(value); + return value; +} +function checkIfSnapshotChanged(inst) { + const latestGetSnapshot = inst.getSnapshot; + const prevValue = inst.value; + try { + const nextValue = latestGetSnapshot(); + return !is(prevValue, nextValue); + } catch (error) { + return true; + } +} + +// src/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts +function useSyncExternalStore2(subscribe, getSnapshot, getServerSnapshot) { + return getSnapshot(); +} + +// src/use-sync-external-store-shim/index.ts +var canUseDOM = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined"); +var isServerEnvironment = !canUseDOM; +var shim = isServerEnvironment ? useSyncExternalStore2 : useSyncExternalStore; +var useSyncExternalStore3 = "useSyncExternalStore" in React11 ? ((r) => r.useSyncExternalStore)(React11) : shim; + +// src/hooks/use-store.ts +function useStore(store) { + return useSyncExternalStore3(store.subscribe, store.getSnapshot, store.getSnapshot); +} + +// src/utils/store.ts +function createStore(initial, actions) { + let state = initial(); + let listeners = /* @__PURE__ */ new Set(); + return { + getSnapshot() { + return state; + }, + subscribe(onChange) { + listeners.add(onChange); + return () => listeners.delete(onChange); + }, + dispatch(key, ...args) { + let newState = actions[key].call(state, ...args); + if (newState) { + state = newState; + listeners.forEach((listener) => listener()); + } + } + }; +} + +// src/hooks/document-overflow/adjust-scrollbar-padding.ts +function adjustScrollbarPadding() { + let scrollbarWidthBefore; + return { + before({ doc }) { + var _a3; + let documentElement = doc.documentElement; + let ownerWindow = (_a3 = doc.defaultView) != null ? _a3 : window; + scrollbarWidthBefore = ownerWindow.innerWidth - documentElement.clientWidth; + }, + after({ doc, d }) { + let documentElement = doc.documentElement; + let scrollbarWidthAfter = documentElement.clientWidth - documentElement.offsetWidth; + let scrollbarWidth = scrollbarWidthBefore - scrollbarWidthAfter; + d.style(documentElement, "paddingRight", `${scrollbarWidth}px`); + } + }; +} + +// src/hooks/document-overflow/handle-ios-locking.ts +function handleIOSLocking() { + if (!isIOS()) { + return {}; + } + let scrollPosition; + return { + before() { + scrollPosition = window.pageYOffset; + }, + after({ doc, d, meta }) { + function inAllowedContainer(el) { + return meta.containers.flatMap((resolve) => resolve()).some((container) => container.contains(el)); + } + d.style(doc.body, "marginTop", `-${scrollPosition}px`); + window.scrollTo(0, 0); + let scrollToElement = null; + d.addEventListener( + doc, + "click", + (e) => { + if (!(e.target instanceof HTMLElement)) { + return; + } + try { + let anchor = e.target.closest("a"); + if (!anchor) + return; + let { hash } = new URL(anchor.href); + let el = doc.querySelector(hash); + if (el && !inAllowedContainer(el)) { + scrollToElement = el; + } + } catch (err) { + } + }, + true + ); + d.addEventListener( + doc, + "touchmove", + (e) => { + if (e.target instanceof HTMLElement && !inAllowedContainer(e.target)) { + e.preventDefault(); + } + }, + { passive: false } + ); + d.add(() => { + window.scrollTo(0, window.pageYOffset + scrollPosition); + if (scrollToElement && scrollToElement.isConnected) { + scrollToElement.scrollIntoView({ block: "nearest" }); + scrollToElement = null; + } + }); + } + }; +} + +// src/hooks/document-overflow/prevent-scroll.ts +function preventScroll() { + return { + before({ doc, d }) { + d.style(doc.documentElement, "overflow", "hidden"); + } + }; +} + +// src/hooks/document-overflow/overflow-store.ts +function buildMeta(fns) { + let tmp = {}; + for (let fn of fns) { + Object.assign(tmp, fn(tmp)); + } + return tmp; +} +var overflows = createStore(() => /* @__PURE__ */ new Map(), { + PUSH(doc, meta) { + var _a3; + let entry = (_a3 = this.get(doc)) != null ? _a3 : { + doc, + count: 0, + d: disposables(), + meta: /* @__PURE__ */ new Set() + }; + entry.count++; + entry.meta.add(meta); + this.set(doc, entry); + return this; + }, + POP(doc, meta) { + let entry = this.get(doc); + if (entry) { + entry.count--; + entry.meta.delete(meta); + } + return this; + }, + SCROLL_PREVENT({ doc, d, meta }) { + let ctx = { + doc, + d, + meta: buildMeta(meta) + }; + let steps = [ + handleIOSLocking(), + adjustScrollbarPadding(), + preventScroll() + ]; + steps.forEach(({ before }) => before == null ? void 0 : before(ctx)); + steps.forEach(({ after }) => after == null ? void 0 : after(ctx)); + }, + SCROLL_ALLOW({ d }) { + d.dispose(); + }, + TEARDOWN({ doc }) { + this.delete(doc); + } +}); +overflows.subscribe(() => { + let docs = overflows.getSnapshot(); + let styles = /* @__PURE__ */ new Map(); + for (let [doc] of docs) { + styles.set(doc, doc.documentElement.style.overflow); + } + for (let entry of docs.values()) { + let isHidden = styles.get(entry.doc) === "hidden"; + let isLocked = entry.count !== 0; + let willChange = isLocked && !isHidden || !isLocked && isHidden; + if (willChange) { + overflows.dispatch(entry.count > 0 ? "SCROLL_PREVENT" : "SCROLL_ALLOW", entry); + } + if (entry.count === 0) { + overflows.dispatch("TEARDOWN", entry); + } + } +}); + +// src/hooks/document-overflow/use-document-overflow.ts +function useDocumentOverflowLockedEffect(doc, shouldBeLocked, meta) { + let store = useStore(overflows); + let entry = doc ? store.get(doc) : void 0; + let locked = entry ? entry.count > 0 : false; + useIsoMorphicEffect(() => { + if (!doc || !shouldBeLocked) { + return; + } + overflows.dispatch("PUSH", doc, meta); + return () => overflows.dispatch("POP", doc, meta); + }, [shouldBeLocked, doc]); + return locked; +} + +// src/hooks/use-inert.tsx +var originals = /* @__PURE__ */ new Map(); +var counts = /* @__PURE__ */ new Map(); +function useInert(node, enabled = true) { + useIsoMorphicEffect(() => { + var _a3; + if (!enabled) + return; + let element = typeof node === "function" ? node() : node.current; + if (!element) + return; + function cleanup() { + var _a4; + if (!element) + return; + let count2 = (_a4 = counts.get(element)) != null ? _a4 : 1; + if (count2 === 1) + counts.delete(element); + else + counts.set(element, count2 - 1); + if (count2 !== 1) + return; + let original = originals.get(element); + if (!original) + return; + if (original["aria-hidden"] === null) + element.removeAttribute("aria-hidden"); + else + element.setAttribute("aria-hidden", original["aria-hidden"]); + element.inert = original.inert; + originals.delete(element); + } + let count = (_a3 = counts.get(element)) != null ? _a3 : 0; + counts.set(element, count + 1); + if (count !== 0) + return cleanup; + originals.set(element, { + "aria-hidden": element.getAttribute("aria-hidden"), + inert: element.inert + }); + element.setAttribute("aria-hidden", "true"); + element.inert = true; + return cleanup; + }, [node, enabled]); +} + +// src/hooks/use-root-containers.tsx +var import_react30 = __toESM(__webpack_require__(/*! react */ "react"), 1); +function useRootContainers({ + defaultContainers = [], + portals +} = {}) { + let mainTreeNodeRef = (0, import_react30.useRef)(null); + let ownerDocument = useOwnerDocument(mainTreeNodeRef); + let resolveContainers2 = useEvent(() => { + var _a3; + let containers = []; + for (let container of defaultContainers) { + if (container === null) + continue; + if (container instanceof HTMLElement) { + containers.push(container); + } else if ("current" in container && container.current instanceof HTMLElement) { + containers.push(container.current); + } + } + if (portals == null ? void 0 : portals.current) { + for (let portal of portals.current) { + containers.push(portal); + } + } + for (let container of (_a3 = ownerDocument == null ? void 0 : ownerDocument.querySelectorAll("html > *, body > *")) != null ? _a3 : []) { + if (container === document.body) + continue; + if (container === document.head) + continue; + if (!(container instanceof HTMLElement)) + continue; + if (container.id === "headlessui-portal-root") + continue; + if (container.contains(mainTreeNodeRef.current)) + continue; + if (containers.some((defaultContainer) => container.contains(defaultContainer))) + continue; + containers.push(container); + } + return containers; + }); + return { + resolveContainers: resolveContainers2, + contains: useEvent( + (element) => resolveContainers2().some((container) => container.contains(element)) + ), + mainTreeNodeRef, + MainTreeNode: (0, import_react30.useMemo)(() => { + return function MainTreeNode() { + return /* @__PURE__ */ import_react30.default.createElement(Hidden, { features: 4 /* Hidden */, ref: mainTreeNodeRef }); + }; + }, [mainTreeNodeRef]) + }; +} + +// src/components/dialog/dialog.tsx +var reducers2 = { + [0 /* SetTitleId */](state, action) { + if (state.titleId === action.id) + return state; + return { ...state, titleId: action.id }; + } +}; +var DialogContext = (0, import_react31.createContext)(null); +DialogContext.displayName = "DialogContext"; +function useDialogContext(component) { + let context = (0, import_react31.useContext)(DialogContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useDialogContext); + throw err; + } + return context; +} +function useScrollLock(ownerDocument, enabled, resolveAllowedContainers = () => [document.body]) { + useDocumentOverflowLockedEffect(ownerDocument, enabled, (meta) => { + var _a3; + return { + containers: [...(_a3 = meta.containers) != null ? _a3 : [], resolveAllowedContainers] + }; + }); +} +function stateReducer2(state, action) { + return match(action.type, reducers2, state, action); +} +var DEFAULT_DIALOG_TAG = "div"; +var DialogRenderFeatures = 1 /* RenderStrategy */ | 2 /* Static */; +function DialogFn(props, ref) { + var _a3; + let internalId = useId(); + let { + id = `headlessui-dialog-${internalId}`, + open, + onClose, + initialFocus, + __demoMode = false, + ...theirProps + } = props; + let [nestedDialogCount, setNestedDialogCount] = (0, import_react31.useState)(0); + let usesOpenClosedState = useOpenClosed(); + if (open === void 0 && usesOpenClosedState !== null) { + open = (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + let internalDialogRef = (0, import_react31.useRef)(null); + let dialogRef = useSyncRefs(internalDialogRef, ref); + let ownerDocument = useOwnerDocument(internalDialogRef); + let hasOpen = props.hasOwnProperty("open") || usesOpenClosedState !== null; + let hasOnClose = props.hasOwnProperty("onClose"); + if (!hasOpen && !hasOnClose) { + throw new Error( + `You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.` + ); + } + if (!hasOpen) { + throw new Error( + `You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.` + ); + } + if (!hasOnClose) { + throw new Error( + `You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.` + ); + } + if (typeof open !== "boolean") { + throw new Error( + `You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${open}` + ); + } + if (typeof onClose !== "function") { + throw new Error( + `You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${onClose}` + ); + } + let dialogState = open ? 0 /* Open */ : 1 /* Closed */; + let [state, dispatch] = (0, import_react31.useReducer)(stateReducer2, { + titleId: null, + descriptionId: null, + panelRef: (0, import_react31.createRef)() + }); + let close = useEvent(() => onClose(false)); + let setTitleId = useEvent((id2) => dispatch({ type: 0 /* SetTitleId */, id: id2 })); + let ready = useServerHandoffComplete(); + let enabled = ready ? __demoMode ? false : dialogState === 0 /* Open */ : false; + let hasNestedDialogs = nestedDialogCount > 1; + let hasParentDialog = (0, import_react31.useContext)(DialogContext) !== null; + let [portals, PortalWrapper] = useNestedPortals(); + let { + resolveContainers: resolveRootContainers, + mainTreeNodeRef, + MainTreeNode + } = useRootContainers({ + portals, + defaultContainers: [(_a3 = state.panelRef.current) != null ? _a3 : internalDialogRef.current] + }); + let position = !hasNestedDialogs ? "leaf" : "parent"; + let isClosing = usesOpenClosedState !== null ? (usesOpenClosedState & 4 /* Closing */) === 4 /* Closing */ : false; + let inertOthersEnabled = (() => { + if (hasParentDialog) + return false; + if (isClosing) + return false; + return enabled; + })(); + let resolveRootOfMainTreeNode = (0, import_react31.useCallback)(() => { + var _a4, _b; + return (_b = Array.from((_a4 = ownerDocument == null ? void 0 : ownerDocument.querySelectorAll("body > *")) != null ? _a4 : []).find((root) => { + if (root.id === "headlessui-portal-root") + return false; + return root.contains(mainTreeNodeRef.current) && root instanceof HTMLElement; + })) != null ? _b : null; + }, [mainTreeNodeRef]); + useInert(resolveRootOfMainTreeNode, inertOthersEnabled); + let inertParentDialogs = (() => { + if (hasNestedDialogs) + return true; + return enabled; + })(); + let resolveRootOfParentDialog = (0, import_react31.useCallback)(() => { + var _a4, _b; + return (_b = Array.from((_a4 = ownerDocument == null ? void 0 : ownerDocument.querySelectorAll("[data-headlessui-portal]")) != null ? _a4 : []).find( + (root) => root.contains(mainTreeNodeRef.current) && root instanceof HTMLElement + )) != null ? _b : null; + }, [mainTreeNodeRef]); + useInert(resolveRootOfParentDialog, inertParentDialogs); + let outsideClickEnabled = (() => { + if (!enabled) + return false; + if (hasNestedDialogs) + return false; + return true; + })(); + useOutsideClick(resolveRootContainers, close, outsideClickEnabled); + let escapeToCloseEnabled = (() => { + if (hasNestedDialogs) + return false; + if (dialogState !== 0 /* Open */) + return false; + return true; + })(); + useEventListener(ownerDocument == null ? void 0 : ownerDocument.defaultView, "keydown", (event) => { + if (!escapeToCloseEnabled) + return; + if (event.defaultPrevented) + return; + if (event.key !== "Escape" /* Escape */) + return; + event.preventDefault(); + event.stopPropagation(); + close(); + }); + let scrollLockEnabled = (() => { + if (isClosing) + return false; + if (dialogState !== 0 /* Open */) + return false; + if (hasParentDialog) + return false; + return true; + })(); + useScrollLock(ownerDocument, scrollLockEnabled, resolveRootContainers); + (0, import_react31.useEffect)(() => { + if (dialogState !== 0 /* Open */) + return; + if (!internalDialogRef.current) + return; + let observer = new ResizeObserver((entries) => { + for (let entry of entries) { + let rect = entry.target.getBoundingClientRect(); + if (rect.x === 0 && rect.y === 0 && rect.width === 0 && rect.height === 0) { + close(); + } + } + }); + observer.observe(internalDialogRef.current); + return () => observer.disconnect(); + }, [dialogState, internalDialogRef, close]); + let [describedby, DescriptionProvider] = useDescriptions(); + let contextBag = (0, import_react31.useMemo)( + () => [{ dialogState, close, setTitleId }, state], + [dialogState, state, close, setTitleId] + ); + let slot = (0, import_react31.useMemo)( + () => ({ open: dialogState === 0 /* Open */ }), + [dialogState] + ); + let ourProps = { + ref: dialogRef, + id, + role: "dialog", + "aria-modal": dialogState === 0 /* Open */ ? true : void 0, + "aria-labelledby": state.titleId, + "aria-describedby": describedby + }; + return /* @__PURE__ */ import_react31.default.createElement( + StackProvider, + { + type: "Dialog", + enabled: dialogState === 0 /* Open */, + element: internalDialogRef, + onUpdate: useEvent((message, type) => { + if (type !== "Dialog") + return; + match(message, { + [0 /* Add */]: () => setNestedDialogCount((count) => count + 1), + [1 /* Remove */]: () => setNestedDialogCount((count) => count - 1) + }); + }) + }, + /* @__PURE__ */ import_react31.default.createElement(ForcePortalRoot, { force: true }, /* @__PURE__ */ import_react31.default.createElement(Portal, null, /* @__PURE__ */ import_react31.default.createElement(DialogContext.Provider, { value: contextBag }, /* @__PURE__ */ import_react31.default.createElement(Portal.Group, { target: internalDialogRef }, /* @__PURE__ */ import_react31.default.createElement(ForcePortalRoot, { force: false }, /* @__PURE__ */ import_react31.default.createElement(DescriptionProvider, { slot, name: "Dialog.Description" }, /* @__PURE__ */ import_react31.default.createElement( + FocusTrap, + { + initialFocus, + containers: resolveRootContainers, + features: enabled ? match(position, { + parent: FocusTrap.features.RestoreFocus, + leaf: FocusTrap.features.All & ~FocusTrap.features.FocusLock + }) : FocusTrap.features.None + }, + /* @__PURE__ */ import_react31.default.createElement(PortalWrapper, null, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_DIALOG_TAG, + features: DialogRenderFeatures, + visible: dialogState === 0 /* Open */, + name: "Dialog" + })) + ))))))), + /* @__PURE__ */ import_react31.default.createElement(MainTreeNode, null) + ); +} +var DEFAULT_OVERLAY_TAG = "div"; +function OverlayFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-dialog-overlay-${internalId}`, ...theirProps } = props; + let [{ dialogState, close }] = useDialogContext("Dialog.Overlay"); + let overlayRef = useSyncRefs(ref); + let handleClick = useEvent((event) => { + if (event.target !== event.currentTarget) + return; + if (isDisabledReactIssue7711(event.currentTarget)) + return event.preventDefault(); + event.preventDefault(); + event.stopPropagation(); + close(); + }); + let slot = (0, import_react31.useMemo)( + () => ({ open: dialogState === 0 /* Open */ }), + [dialogState] + ); + let ourProps = { + ref: overlayRef, + id, + "aria-hidden": true, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OVERLAY_TAG, + name: "Dialog.Overlay" + }); +} +var DEFAULT_BACKDROP_TAG = "div"; +function BackdropFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-dialog-backdrop-${internalId}`, ...theirProps } = props; + let [{ dialogState }, state] = useDialogContext("Dialog.Backdrop"); + let backdropRef = useSyncRefs(ref); + (0, import_react31.useEffect)(() => { + if (state.panelRef.current === null) { + throw new Error( + `A component is being used, but a component is missing.` + ); + } + }, [state.panelRef]); + let slot = (0, import_react31.useMemo)( + () => ({ open: dialogState === 0 /* Open */ }), + [dialogState] + ); + let ourProps = { + ref: backdropRef, + id, + "aria-hidden": true + }; + return /* @__PURE__ */ import_react31.default.createElement(ForcePortalRoot, { force: true }, /* @__PURE__ */ import_react31.default.createElement(Portal, null, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BACKDROP_TAG, + name: "Dialog.Backdrop" + }))); +} +var DEFAULT_PANEL_TAG = "div"; +function PanelFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-dialog-panel-${internalId}`, ...theirProps } = props; + let [{ dialogState }, state] = useDialogContext("Dialog.Panel"); + let panelRef = useSyncRefs(ref, state.panelRef); + let slot = (0, import_react31.useMemo)( + () => ({ open: dialogState === 0 /* Open */ }), + [dialogState] + ); + let handleClick = useEvent((event) => { + event.stopPropagation(); + }); + let ourProps = { + ref: panelRef, + id, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_PANEL_TAG, + name: "Dialog.Panel" + }); +} +var DEFAULT_TITLE_TAG = "h2"; +function TitleFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-dialog-title-${internalId}`, ...theirProps } = props; + let [{ dialogState, setTitleId }] = useDialogContext("Dialog.Title"); + let titleRef = useSyncRefs(ref); + (0, import_react31.useEffect)(() => { + setTitleId(id); + return () => setTitleId(null); + }, [id, setTitleId]); + let slot = (0, import_react31.useMemo)( + () => ({ open: dialogState === 0 /* Open */ }), + [dialogState] + ); + let ourProps = { ref: titleRef, id }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_TITLE_TAG, + name: "Dialog.Title" + }); +} +var DialogRoot = forwardRefWithAs(DialogFn); +var Backdrop = forwardRefWithAs(BackdropFn); +var Panel = forwardRefWithAs(PanelFn); +var Overlay = forwardRefWithAs(OverlayFn); +var Title = forwardRefWithAs(TitleFn); +var Dialog = Object.assign(DialogRoot, { + Backdrop, + Panel, + Overlay, + Title, + Description +}); + +// src/components/disclosure/disclosure.tsx +var import_react33 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/utils/start-transition.ts +var import_react32 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var _a2; +var startTransition = ( + // Prefer React's `startTransition` if it's available. + // @ts-expect-error - `startTransition` doesn't exist in React < 18. + (_a2 = import_react32.default.startTransition) != null ? _a2 : function startTransition2(cb) { + cb(); + } +); + +// src/components/disclosure/disclosure.tsx +var reducers3 = { + [0 /* ToggleDisclosure */]: (state) => ({ + ...state, + disclosureState: match(state.disclosureState, { + [0 /* Open */]: 1 /* Closed */, + [1 /* Closed */]: 0 /* Open */ + }) + }), + [1 /* CloseDisclosure */]: (state) => { + if (state.disclosureState === 1 /* Closed */) + return state; + return { ...state, disclosureState: 1 /* Closed */ }; + }, + [4 /* LinkPanel */](state) { + if (state.linkedPanel === true) + return state; + return { ...state, linkedPanel: true }; + }, + [5 /* UnlinkPanel */](state) { + if (state.linkedPanel === false) + return state; + return { ...state, linkedPanel: false }; + }, + [2 /* SetButtonId */](state, action) { + if (state.buttonId === action.buttonId) + return state; + return { ...state, buttonId: action.buttonId }; + }, + [3 /* SetPanelId */](state, action) { + if (state.panelId === action.panelId) + return state; + return { ...state, panelId: action.panelId }; + } +}; +var DisclosureContext = (0, import_react33.createContext)(null); +DisclosureContext.displayName = "DisclosureContext"; +function useDisclosureContext(component) { + let context = (0, import_react33.useContext)(DisclosureContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useDisclosureContext); + throw err; + } + return context; +} +var DisclosureAPIContext = (0, import_react33.createContext)(null); +DisclosureAPIContext.displayName = "DisclosureAPIContext"; +function useDisclosureAPIContext(component) { + let context = (0, import_react33.useContext)(DisclosureAPIContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useDisclosureAPIContext); + throw err; + } + return context; +} +var DisclosurePanelContext = (0, import_react33.createContext)(null); +DisclosurePanelContext.displayName = "DisclosurePanelContext"; +function useDisclosurePanelContext() { + return (0, import_react33.useContext)(DisclosurePanelContext); +} +function stateReducer3(state, action) { + return match(action.type, reducers3, state, action); +} +var DEFAULT_DISCLOSURE_TAG = import_react33.Fragment; +function DisclosureFn(props, ref) { + let { defaultOpen = false, ...theirProps } = props; + let internalDisclosureRef = (0, import_react33.useRef)(null); + let disclosureRef = useSyncRefs( + ref, + optionalRef( + (ref2) => { + internalDisclosureRef.current = ref2; + }, + props.as === void 0 || // @ts-expect-error The `as` prop _can_ be a Fragment + props.as === import_react33.Fragment + ) + ); + let panelRef = (0, import_react33.useRef)(null); + let buttonRef = (0, import_react33.useRef)(null); + let reducerBag = (0, import_react33.useReducer)(stateReducer3, { + disclosureState: defaultOpen ? 0 /* Open */ : 1 /* Closed */, + linkedPanel: false, + buttonRef, + panelRef, + buttonId: null, + panelId: null + }); + let [{ disclosureState, buttonId }, dispatch] = reducerBag; + let close = useEvent((focusableElement) => { + dispatch({ type: 1 /* CloseDisclosure */ }); + let ownerDocument = getOwnerDocument(internalDisclosureRef); + if (!ownerDocument) + return; + if (!buttonId) + return; + let restoreElement = (() => { + if (!focusableElement) + return ownerDocument.getElementById(buttonId); + if (focusableElement instanceof HTMLElement) + return focusableElement; + if (focusableElement.current instanceof HTMLElement) + return focusableElement.current; + return ownerDocument.getElementById(buttonId); + })(); + restoreElement == null ? void 0 : restoreElement.focus(); + }); + let api = (0, import_react33.useMemo)(() => ({ close }), [close]); + let slot = (0, import_react33.useMemo)( + () => ({ open: disclosureState === 0 /* Open */, close }), + [disclosureState, close] + ); + let ourProps = { + ref: disclosureRef + }; + return /* @__PURE__ */ import_react33.default.createElement(DisclosureContext.Provider, { value: reducerBag }, /* @__PURE__ */ import_react33.default.createElement(DisclosureAPIContext.Provider, { value: api }, /* @__PURE__ */ import_react33.default.createElement( + OpenClosedProvider, + { + value: match(disclosureState, { + [0 /* Open */]: 1 /* Open */, + [1 /* Closed */]: 2 /* Closed */ + }) + }, + render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_DISCLOSURE_TAG, + name: "Disclosure" + }) + ))); +} +var DEFAULT_BUTTON_TAG2 = "button"; +function ButtonFn2(props, ref) { + let internalId = useId(); + let { id = `headlessui-disclosure-button-${internalId}`, ...theirProps } = props; + let [state, dispatch] = useDisclosureContext("Disclosure.Button"); + let panelContext = useDisclosurePanelContext(); + let isWithinPanel = panelContext === null ? false : panelContext === state.panelId; + let internalButtonRef = (0, import_react33.useRef)(null); + let buttonRef = useSyncRefs(internalButtonRef, ref, !isWithinPanel ? state.buttonRef : null); + (0, import_react33.useEffect)(() => { + if (isWithinPanel) + return; + dispatch({ type: 2 /* SetButtonId */, buttonId: id }); + return () => { + dispatch({ type: 2 /* SetButtonId */, buttonId: null }); + }; + }, [id, dispatch, isWithinPanel]); + let handleKeyDown = useEvent((event) => { + var _a3; + if (isWithinPanel) { + if (state.disclosureState === 1 /* Closed */) + return; + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 0 /* ToggleDisclosure */ }); + (_a3 = state.buttonRef.current) == null ? void 0 : _a3.focus(); + break; + } + } else { + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 0 /* ToggleDisclosure */ }); + break; + } + } + }); + let handleKeyUp = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + event.preventDefault(); + break; + } + }); + let handleClick = useEvent((event) => { + var _a3; + if (isDisabledReactIssue7711(event.currentTarget)) + return; + if (props.disabled) + return; + if (isWithinPanel) { + dispatch({ type: 0 /* ToggleDisclosure */ }); + (_a3 = state.buttonRef.current) == null ? void 0 : _a3.focus(); + } else { + dispatch({ type: 0 /* ToggleDisclosure */ }); + } + }); + let slot = (0, import_react33.useMemo)( + () => ({ open: state.disclosureState === 0 /* Open */ }), + [state] + ); + let type = useResolveButtonType(props, internalButtonRef); + let ourProps = isWithinPanel ? { ref: buttonRef, type, onKeyDown: handleKeyDown, onClick: handleClick } : { + ref: buttonRef, + id, + type, + "aria-expanded": props.disabled ? void 0 : state.disclosureState === 0 /* Open */, + "aria-controls": state.linkedPanel ? state.panelId : void 0, + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG2, + name: "Disclosure.Button" + }); +} +var DEFAULT_PANEL_TAG2 = "div"; +var PanelRenderFeatures = 1 /* RenderStrategy */ | 2 /* Static */; +function PanelFn2(props, ref) { + let internalId = useId(); + let { id = `headlessui-disclosure-panel-${internalId}`, ...theirProps } = props; + let [state, dispatch] = useDisclosureContext("Disclosure.Panel"); + let { close } = useDisclosureAPIContext("Disclosure.Panel"); + let panelRef = useSyncRefs(ref, state.panelRef, (el) => { + startTransition(() => dispatch({ type: el ? 4 /* LinkPanel */ : 5 /* UnlinkPanel */ })); + }); + (0, import_react33.useEffect)(() => { + dispatch({ type: 3 /* SetPanelId */, panelId: id }); + return () => { + dispatch({ type: 3 /* SetPanelId */, panelId: null }); + }; + }, [id, dispatch]); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return state.disclosureState === 0 /* Open */; + })(); + let slot = (0, import_react33.useMemo)( + () => ({ open: state.disclosureState === 0 /* Open */, close }), + [state, close] + ); + let ourProps = { + ref: panelRef, + id + }; + return /* @__PURE__ */ import_react33.default.createElement(DisclosurePanelContext.Provider, { value: state.panelId }, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_PANEL_TAG2, + features: PanelRenderFeatures, + visible, + name: "Disclosure.Panel" + })); +} +var DisclosureRoot = forwardRefWithAs(DisclosureFn); +var Button2 = forwardRefWithAs(ButtonFn2); +var Panel2 = forwardRefWithAs(PanelFn2); +var Disclosure = Object.assign(DisclosureRoot, { Button: Button2, Panel: Panel2 }); + +// src/components/listbox/listbox.tsx +var import_react35 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/hooks/use-text-value.ts +var import_react34 = __webpack_require__(/*! react */ "react"); + +// src/utils/get-text-value.ts +var emojiRegex = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g; +function getTextContents(element) { + var _a3, _b; + let currentInnerText = (_a3 = element.innerText) != null ? _a3 : ""; + let copy = element.cloneNode(true); + if (!(copy instanceof HTMLElement)) { + return currentInnerText; + } + let dropped = false; + for (let child of copy.querySelectorAll('[hidden],[aria-hidden],[role="img"]')) { + child.remove(); + dropped = true; + } + let value = dropped ? (_b = copy.innerText) != null ? _b : "" : currentInnerText; + if (emojiRegex.test(value)) { + value = value.replace(emojiRegex, ""); + } + return value; +} +function getTextValue(element) { + let label = element.getAttribute("aria-label"); + if (typeof label === "string") + return label.trim(); + let labelledby = element.getAttribute("aria-labelledby"); + if (labelledby) { + let labels = labelledby.split(" ").map((labelledby2) => { + let labelEl = document.getElementById(labelledby2); + if (labelEl) { + let label2 = labelEl.getAttribute("aria-label"); + if (typeof label2 === "string") + return label2.trim(); + return getTextContents(labelEl).trim(); + } + return null; + }).filter(Boolean); + if (labels.length > 0) + return labels.join(", "); + } + return getTextContents(element).trim(); +} + +// src/hooks/use-text-value.ts +function useTextValue(element) { + let cacheKey = (0, import_react34.useRef)(""); + let cacheValue = (0, import_react34.useRef)(""); + return useEvent(() => { + let el = element.current; + if (!el) + return ""; + let currentKey = el.innerText; + if (cacheKey.current === currentKey) { + return cacheValue.current; + } + let value = getTextValue(el).trim().toLowerCase(); + cacheKey.current = currentKey; + cacheValue.current = value; + return value; + }); +} + +// src/components/listbox/listbox.tsx +function adjustOrderedState2(state, adjustment = (i) => i) { + let currentActiveOption = state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null; + let sortedOptions = sortByDomNode( + adjustment(state.options.slice()), + (option) => option.dataRef.current.domRef.current + ); + let adjustedActiveOptionIndex = currentActiveOption ? sortedOptions.indexOf(currentActiveOption) : null; + if (adjustedActiveOptionIndex === -1) { + adjustedActiveOptionIndex = null; + } + return { + options: sortedOptions, + activeOptionIndex: adjustedActiveOptionIndex + }; +} +var reducers4 = { + [1 /* CloseListbox */](state) { + if (state.dataRef.current.disabled) + return state; + if (state.listboxState === 1 /* Closed */) + return state; + return { ...state, activeOptionIndex: null, listboxState: 1 /* Closed */ }; + }, + [0 /* OpenListbox */](state) { + if (state.dataRef.current.disabled) + return state; + if (state.listboxState === 0 /* Open */) + return state; + let activeOptionIndex = state.activeOptionIndex; + let { isSelected } = state.dataRef.current; + let optionIdx = state.options.findIndex((option) => isSelected(option.dataRef.current.value)); + if (optionIdx !== -1) { + activeOptionIndex = optionIdx; + } + return { ...state, listboxState: 0 /* Open */, activeOptionIndex }; + }, + [2 /* GoToOption */](state, action) { + var _a3; + if (state.dataRef.current.disabled) + return state; + if (state.listboxState === 1 /* Closed */) + return state; + let adjustedState = adjustOrderedState2(state); + let activeOptionIndex = calculateActiveIndex(action, { + resolveItems: () => adjustedState.options, + resolveActiveIndex: () => adjustedState.activeOptionIndex, + resolveId: (option) => option.id, + resolveDisabled: (option) => option.dataRef.current.disabled + }); + return { + ...state, + ...adjustedState, + searchQuery: "", + activeOptionIndex, + activationTrigger: (_a3 = action.trigger) != null ? _a3 : 1 /* Other */ + }; + }, + [3 /* Search */]: (state, action) => { + if (state.dataRef.current.disabled) + return state; + if (state.listboxState === 1 /* Closed */) + return state; + let wasAlreadySearching = state.searchQuery !== ""; + let offset = wasAlreadySearching ? 0 : 1; + let searchQuery = state.searchQuery + action.value.toLowerCase(); + let reOrderedOptions = state.activeOptionIndex !== null ? state.options.slice(state.activeOptionIndex + offset).concat(state.options.slice(0, state.activeOptionIndex + offset)) : state.options; + let matchingOption = reOrderedOptions.find( + (option) => { + var _a3; + return !option.dataRef.current.disabled && ((_a3 = option.dataRef.current.textValue) == null ? void 0 : _a3.startsWith(searchQuery)); + } + ); + let matchIdx = matchingOption ? state.options.indexOf(matchingOption) : -1; + if (matchIdx === -1 || matchIdx === state.activeOptionIndex) + return { ...state, searchQuery }; + return { + ...state, + searchQuery, + activeOptionIndex: matchIdx, + activationTrigger: 1 /* Other */ + }; + }, + [4 /* ClearSearch */](state) { + if (state.dataRef.current.disabled) + return state; + if (state.listboxState === 1 /* Closed */) + return state; + if (state.searchQuery === "") + return state; + return { ...state, searchQuery: "" }; + }, + [5 /* RegisterOption */]: (state, action) => { + let option = { id: action.id, dataRef: action.dataRef }; + let adjustedState = adjustOrderedState2(state, (options) => [...options, option]); + if (state.activeOptionIndex === null) { + if (state.dataRef.current.isSelected(action.dataRef.current.value)) { + adjustedState.activeOptionIndex = adjustedState.options.indexOf(option); + } + } + return { ...state, ...adjustedState }; + }, + [6 /* UnregisterOption */]: (state, action) => { + let adjustedState = adjustOrderedState2(state, (options) => { + let idx = options.findIndex((a) => a.id === action.id); + if (idx !== -1) + options.splice(idx, 1); + return options; + }); + return { + ...state, + ...adjustedState, + activationTrigger: 1 /* Other */ + }; + }, + [7 /* RegisterLabel */]: (state, action) => { + return { + ...state, + labelId: action.id + }; + } +}; +var ListboxActionsContext = (0, import_react35.createContext)(null); +ListboxActionsContext.displayName = "ListboxActionsContext"; +function useActions2(component) { + let context = (0, import_react35.useContext)(ListboxActionsContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useActions2); + throw err; + } + return context; +} +var ListboxDataContext = (0, import_react35.createContext)(null); +ListboxDataContext.displayName = "ListboxDataContext"; +function useData2(component) { + let context = (0, import_react35.useContext)(ListboxDataContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useData2); + throw err; + } + return context; +} +function stateReducer4(state, action) { + return match(action.type, reducers4, state, action); +} +var DEFAULT_LISTBOX_TAG = import_react35.Fragment; +function ListboxFn(props, ref) { + let { + value: controlledValue, + defaultValue, + form: formName, + name, + onChange: controlledOnChange, + by = (a, z) => a === z, + disabled = false, + horizontal = false, + multiple = false, + ...theirProps + } = props; + const orientation = horizontal ? "horizontal" : "vertical"; + let listboxRef = useSyncRefs(ref); + let [value = multiple ? [] : void 0, theirOnChange] = useControllable( + controlledValue, + controlledOnChange, + defaultValue + ); + let [state, dispatch] = (0, import_react35.useReducer)(stateReducer4, { + dataRef: (0, import_react35.createRef)(), + listboxState: 1 /* Closed */, + options: [], + searchQuery: "", + labelId: null, + activeOptionIndex: null, + activationTrigger: 1 /* Other */ + }); + let optionsPropsRef = (0, import_react35.useRef)({ static: false, hold: false }); + let labelRef = (0, import_react35.useRef)(null); + let buttonRef = (0, import_react35.useRef)(null); + let optionsRef = (0, import_react35.useRef)(null); + let compare = useEvent( + typeof by === "string" ? (a, z) => { + let property = by; + return (a == null ? void 0 : a[property]) === (z == null ? void 0 : z[property]); + } : by + ); + let isSelected = (0, import_react35.useCallback)( + (compareValue) => match(data.mode, { + [1 /* Multi */]: () => value.some((option) => compare(option, compareValue)), + [0 /* Single */]: () => compare(value, compareValue) + }), + [value] + ); + let data = (0, import_react35.useMemo)( + () => ({ + ...state, + value, + disabled, + mode: multiple ? 1 /* Multi */ : 0 /* Single */, + orientation, + compare, + isSelected, + optionsPropsRef, + labelRef, + buttonRef, + optionsRef + }), + [value, disabled, multiple, state] + ); + useIsoMorphicEffect(() => { + state.dataRef.current = data; + }, [data]); + useOutsideClick( + [data.buttonRef, data.optionsRef], + (event, target) => { + var _a3; + dispatch({ type: 1 /* CloseListbox */ }); + if (!isFocusableElement(target, 1 /* Loose */)) { + event.preventDefault(); + (_a3 = data.buttonRef.current) == null ? void 0 : _a3.focus(); + } + }, + data.listboxState === 0 /* Open */ + ); + let slot = (0, import_react35.useMemo)( + () => ({ open: data.listboxState === 0 /* Open */, disabled, value }), + [data, disabled, value] + ); + let selectOption = useEvent((id) => { + let option = data.options.find((item) => item.id === id); + if (!option) + return; + onChange(option.dataRef.current.value); + }); + let selectActiveOption = useEvent(() => { + if (data.activeOptionIndex !== null) { + let { dataRef, id } = data.options[data.activeOptionIndex]; + onChange(dataRef.current.value); + dispatch({ type: 2 /* GoToOption */, focus: 4 /* Specific */, id }); + } + }); + let openListbox = useEvent(() => dispatch({ type: 0 /* OpenListbox */ })); + let closeListbox = useEvent(() => dispatch({ type: 1 /* CloseListbox */ })); + let goToOption = useEvent((focus, id, trigger) => { + if (focus === 4 /* Specific */) { + return dispatch({ type: 2 /* GoToOption */, focus: 4 /* Specific */, id, trigger }); + } + return dispatch({ type: 2 /* GoToOption */, focus, trigger }); + }); + let registerOption = useEvent((id, dataRef) => { + dispatch({ type: 5 /* RegisterOption */, id, dataRef }); + return () => dispatch({ type: 6 /* UnregisterOption */, id }); + }); + let registerLabel = useEvent((id) => { + dispatch({ type: 7 /* RegisterLabel */, id }); + return () => dispatch({ type: 7 /* RegisterLabel */, id: null }); + }); + let onChange = useEvent((value2) => { + return match(data.mode, { + [0 /* Single */]() { + return theirOnChange == null ? void 0 : theirOnChange(value2); + }, + [1 /* Multi */]() { + let copy = data.value.slice(); + let idx = copy.findIndex((item) => compare(item, value2)); + if (idx === -1) { + copy.push(value2); + } else { + copy.splice(idx, 1); + } + return theirOnChange == null ? void 0 : theirOnChange(copy); + } + }); + }); + let search = useEvent((value2) => dispatch({ type: 3 /* Search */, value: value2 })); + let clearSearch = useEvent(() => dispatch({ type: 4 /* ClearSearch */ })); + let actions = (0, import_react35.useMemo)( + () => ({ + onChange, + registerOption, + registerLabel, + goToOption, + closeListbox, + openListbox, + selectActiveOption, + selectOption, + search, + clearSearch + }), + [] + ); + let ourProps = { ref: listboxRef }; + let form = (0, import_react35.useRef)(null); + let d = useDisposables(); + (0, import_react35.useEffect)(() => { + if (!form.current) + return; + if (defaultValue === void 0) + return; + d.addEventListener(form.current, "reset", () => { + onChange(defaultValue); + }); + }, [ + form, + onChange + /* Explicitly ignoring `defaultValue` */ + ]); + return /* @__PURE__ */ import_react35.default.createElement(ListboxActionsContext.Provider, { value: actions }, /* @__PURE__ */ import_react35.default.createElement(ListboxDataContext.Provider, { value: data }, /* @__PURE__ */ import_react35.default.createElement( + OpenClosedProvider, + { + value: match(data.listboxState, { + [0 /* Open */]: 1 /* Open */, + [1 /* Closed */]: 2 /* Closed */ + }) + }, + name != null && value != null && objectToFormEntries({ [name]: value }).map(([name2, value2], idx) => /* @__PURE__ */ import_react35.default.createElement( + Hidden, + { + features: 4 /* Hidden */, + ref: idx === 0 ? (element) => { + var _a3; + form.current = (_a3 = element == null ? void 0 : element.closest("form")) != null ? _a3 : null; + } : void 0, + ...compact({ + key: name2, + as: "input", + type: "hidden", + hidden: true, + readOnly: true, + form: formName, + name: name2, + value: value2 + }) + } + )), + render({ ourProps, theirProps, slot, defaultTag: DEFAULT_LISTBOX_TAG, name: "Listbox" }) + ))); +} +var DEFAULT_BUTTON_TAG3 = "button"; +function ButtonFn3(props, ref) { + var _a3; + let internalId = useId(); + let { id = `headlessui-listbox-button-${internalId}`, ...theirProps } = props; + let data = useData2("Listbox.Button"); + let actions = useActions2("Listbox.Button"); + let buttonRef = useSyncRefs(data.buttonRef, ref); + let d = useDisposables(); + let handleKeyDown = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + case "ArrowDown" /* ArrowDown */: + event.preventDefault(); + actions.openListbox(); + d.nextFrame(() => { + if (!data.value) + actions.goToOption(0 /* First */); + }); + break; + case "ArrowUp" /* ArrowUp */: + event.preventDefault(); + actions.openListbox(); + d.nextFrame(() => { + if (!data.value) + actions.goToOption(3 /* Last */); + }); + break; + } + }); + let handleKeyUp = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + event.preventDefault(); + break; + } + }); + let handleClick = useEvent((event) => { + if (isDisabledReactIssue7711(event.currentTarget)) + return event.preventDefault(); + if (data.listboxState === 0 /* Open */) { + actions.closeListbox(); + d.nextFrame(() => { + var _a4; + return (_a4 = data.buttonRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + } else { + event.preventDefault(); + actions.openListbox(); + } + }); + let labelledby = useComputed(() => { + if (!data.labelId) + return void 0; + return [data.labelId, id].join(" "); + }, [data.labelId, id]); + let slot = (0, import_react35.useMemo)( + () => ({ + open: data.listboxState === 0 /* Open */, + disabled: data.disabled, + value: data.value + }), + [data] + ); + let ourProps = { + ref: buttonRef, + id, + type: useResolveButtonType(props, data.buttonRef), + "aria-haspopup": "listbox", + "aria-controls": (_a3 = data.optionsRef.current) == null ? void 0 : _a3.id, + "aria-expanded": data.disabled ? void 0 : data.listboxState === 0 /* Open */, + "aria-labelledby": labelledby, + disabled: data.disabled, + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG3, + name: "Listbox.Button" + }); +} +var DEFAULT_LABEL_TAG2 = "label"; +function LabelFn2(props, ref) { + let internalId = useId(); + let { id = `headlessui-listbox-label-${internalId}`, ...theirProps } = props; + let data = useData2("Listbox.Label"); + let actions = useActions2("Listbox.Label"); + let labelRef = useSyncRefs(data.labelRef, ref); + useIsoMorphicEffect(() => actions.registerLabel(id), [id]); + let handleClick = useEvent(() => { + var _a3; + return (_a3 = data.buttonRef.current) == null ? void 0 : _a3.focus({ preventScroll: true }); + }); + let slot = (0, import_react35.useMemo)( + () => ({ open: data.listboxState === 0 /* Open */, disabled: data.disabled }), + [data] + ); + let ourProps = { ref: labelRef, id, onClick: handleClick }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_LABEL_TAG2, + name: "Listbox.Label" + }); +} +var DEFAULT_OPTIONS_TAG2 = "ul"; +var OptionsRenderFeatures2 = 1 /* RenderStrategy */ | 2 /* Static */; +function OptionsFn2(props, ref) { + var _a3; + let internalId = useId(); + let { id = `headlessui-listbox-options-${internalId}`, ...theirProps } = props; + let data = useData2("Listbox.Options"); + let actions = useActions2("Listbox.Options"); + let optionsRef = useSyncRefs(data.optionsRef, ref); + let d = useDisposables(); + let searchDisposables = useDisposables(); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return data.listboxState === 0 /* Open */; + })(); + (0, import_react35.useEffect)(() => { + var _a4; + let container = data.optionsRef.current; + if (!container) + return; + if (data.listboxState !== 0 /* Open */) + return; + if (container === ((_a4 = getOwnerDocument(container)) == null ? void 0 : _a4.activeElement)) + return; + container.focus({ preventScroll: true }); + }, [data.listboxState, data.optionsRef]); + let handleKeyDown = useEvent((event) => { + searchDisposables.dispose(); + switch (event.key) { + case " " /* Space */: + if (data.searchQuery !== "") { + event.preventDefault(); + event.stopPropagation(); + return actions.search(event.key); + } + case "Enter" /* Enter */: + event.preventDefault(); + event.stopPropagation(); + if (data.activeOptionIndex !== null) { + let { dataRef } = data.options[data.activeOptionIndex]; + actions.onChange(dataRef.current.value); + } + if (data.mode === 0 /* Single */) { + actions.closeListbox(); + disposables().nextFrame(() => { + var _a4; + return (_a4 = data.buttonRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + } + break; + case match(data.orientation, { vertical: "ArrowDown" /* ArrowDown */, horizontal: "ArrowRight" /* ArrowRight */ }): + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(2 /* Next */); + case match(data.orientation, { vertical: "ArrowUp" /* ArrowUp */, horizontal: "ArrowLeft" /* ArrowLeft */ }): + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(1 /* Previous */); + case "Home" /* Home */: + case "PageUp" /* PageUp */: + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(0 /* First */); + case "End" /* End */: + case "PageDown" /* PageDown */: + event.preventDefault(); + event.stopPropagation(); + return actions.goToOption(3 /* Last */); + case "Escape" /* Escape */: + event.preventDefault(); + event.stopPropagation(); + actions.closeListbox(); + return d.nextFrame(() => { + var _a4; + return (_a4 = data.buttonRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + case "Tab" /* Tab */: + event.preventDefault(); + event.stopPropagation(); + break; + default: + if (event.key.length === 1) { + actions.search(event.key); + searchDisposables.setTimeout(() => actions.clearSearch(), 350); + } + break; + } + }); + let labelledby = useComputed( + () => { + var _a4, _b, _c; + return (_c = (_a4 = data.labelRef.current) == null ? void 0 : _a4.id) != null ? _c : (_b = data.buttonRef.current) == null ? void 0 : _b.id; + }, + [data.labelRef.current, data.buttonRef.current] + ); + let slot = (0, import_react35.useMemo)( + () => ({ open: data.listboxState === 0 /* Open */ }), + [data] + ); + let ourProps = { + "aria-activedescendant": data.activeOptionIndex === null ? void 0 : (_a3 = data.options[data.activeOptionIndex]) == null ? void 0 : _a3.id, + "aria-multiselectable": data.mode === 1 /* Multi */ ? true : void 0, + "aria-labelledby": labelledby, + "aria-orientation": data.orientation, + id, + onKeyDown: handleKeyDown, + role: "listbox", + tabIndex: 0, + ref: optionsRef + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OPTIONS_TAG2, + features: OptionsRenderFeatures2, + visible, + name: "Listbox.Options" + }); +} +var DEFAULT_OPTION_TAG2 = "li"; +function OptionFn2(props, ref) { + let internalId = useId(); + let { + id = `headlessui-listbox-option-${internalId}`, + disabled = false, + value, + ...theirProps + } = props; + let data = useData2("Listbox.Option"); + let actions = useActions2("Listbox.Option"); + let active = data.activeOptionIndex !== null ? data.options[data.activeOptionIndex].id === id : false; + let selected = data.isSelected(value); + let internalOptionRef = (0, import_react35.useRef)(null); + let getTextValue2 = useTextValue(internalOptionRef); + let bag = useLatestValue({ + disabled, + value, + domRef: internalOptionRef, + get textValue() { + return getTextValue2(); + } + }); + let optionRef = useSyncRefs(ref, internalOptionRef); + useIsoMorphicEffect(() => { + if (data.listboxState !== 0 /* Open */) + return; + if (!active) + return; + if (data.activationTrigger === 0 /* Pointer */) + return; + let d = disposables(); + d.requestAnimationFrame(() => { + var _a3, _b; + (_b = (_a3 = internalOptionRef.current) == null ? void 0 : _a3.scrollIntoView) == null ? void 0 : _b.call(_a3, { block: "nearest" }); + }); + return d.dispose; + }, [ + internalOptionRef, + active, + data.listboxState, + data.activationTrigger, + /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ + data.activeOptionIndex + ]); + useIsoMorphicEffect(() => actions.registerOption(id, bag), [bag, id]); + let handleClick = useEvent((event) => { + if (disabled) + return event.preventDefault(); + actions.onChange(value); + if (data.mode === 0 /* Single */) { + actions.closeListbox(); + disposables().nextFrame(() => { + var _a3; + return (_a3 = data.buttonRef.current) == null ? void 0 : _a3.focus({ preventScroll: true }); + }); + } + }); + let handleFocus = useEvent(() => { + if (disabled) + return actions.goToOption(5 /* Nothing */); + actions.goToOption(4 /* Specific */, id); + }); + let pointer = useTrackedPointer(); + let handleEnter = useEvent((evt) => pointer.update(evt)); + let handleMove = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (active) + return; + actions.goToOption(4 /* Specific */, id, 0 /* Pointer */); + }); + let handleLeave = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (!active) + return; + actions.goToOption(5 /* Nothing */); + }); + let slot = (0, import_react35.useMemo)( + () => ({ active, selected, disabled }), + [active, selected, disabled] + ); + let ourProps = { + id, + ref: optionRef, + role: "option", + tabIndex: disabled === true ? void 0 : -1, + "aria-disabled": disabled === true ? true : void 0, + // According to the WAI-ARIA best practices, we should use aria-checked for + // multi-select,but Voice-Over disagrees. So we use aria-checked instead for + // both single and multi-select. + "aria-selected": selected, + disabled: void 0, + // Never forward the `disabled` prop + onClick: handleClick, + onFocus: handleFocus, + onPointerEnter: handleEnter, + onMouseEnter: handleEnter, + onPointerMove: handleMove, + onMouseMove: handleMove, + onPointerLeave: handleLeave, + onMouseLeave: handleLeave + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OPTION_TAG2, + name: "Listbox.Option" + }); +} +var ListboxRoot = forwardRefWithAs(ListboxFn); +var Button3 = forwardRefWithAs(ButtonFn3); +var Label2 = forwardRefWithAs(LabelFn2); +var Options2 = forwardRefWithAs(OptionsFn2); +var Option2 = forwardRefWithAs(OptionFn2); +var Listbox = Object.assign(ListboxRoot, { Button: Button3, Label: Label2, Options: Options2, Option: Option2 }); + +// src/components/menu/menu.tsx +var import_react36 = __toESM(__webpack_require__(/*! react */ "react"), 1); +function adjustOrderedState3(state, adjustment = (i) => i) { + let currentActiveItem = state.activeItemIndex !== null ? state.items[state.activeItemIndex] : null; + let sortedItems = sortByDomNode( + adjustment(state.items.slice()), + (item) => item.dataRef.current.domRef.current + ); + let adjustedActiveItemIndex = currentActiveItem ? sortedItems.indexOf(currentActiveItem) : null; + if (adjustedActiveItemIndex === -1) { + adjustedActiveItemIndex = null; + } + return { + items: sortedItems, + activeItemIndex: adjustedActiveItemIndex + }; +} +var reducers5 = { + [1 /* CloseMenu */](state) { + if (state.menuState === 1 /* Closed */) + return state; + return { ...state, activeItemIndex: null, menuState: 1 /* Closed */ }; + }, + [0 /* OpenMenu */](state) { + if (state.menuState === 0 /* Open */) + return state; + return { + ...state, + /* We can turn off demo mode once we re-open the `Menu` */ + __demoMode: false, + menuState: 0 /* Open */ + }; + }, + [2 /* GoToItem */]: (state, action) => { + var _a3; + let adjustedState = adjustOrderedState3(state); + let activeItemIndex = calculateActiveIndex(action, { + resolveItems: () => adjustedState.items, + resolveActiveIndex: () => adjustedState.activeItemIndex, + resolveId: (item) => item.id, + resolveDisabled: (item) => item.dataRef.current.disabled + }); + return { + ...state, + ...adjustedState, + searchQuery: "", + activeItemIndex, + activationTrigger: (_a3 = action.trigger) != null ? _a3 : 1 /* Other */ + }; + }, + [3 /* Search */]: (state, action) => { + let wasAlreadySearching = state.searchQuery !== ""; + let offset = wasAlreadySearching ? 0 : 1; + let searchQuery = state.searchQuery + action.value.toLowerCase(); + let reOrderedItems = state.activeItemIndex !== null ? state.items.slice(state.activeItemIndex + offset).concat(state.items.slice(0, state.activeItemIndex + offset)) : state.items; + let matchingItem = reOrderedItems.find( + (item) => { + var _a3; + return ((_a3 = item.dataRef.current.textValue) == null ? void 0 : _a3.startsWith(searchQuery)) && !item.dataRef.current.disabled; + } + ); + let matchIdx = matchingItem ? state.items.indexOf(matchingItem) : -1; + if (matchIdx === -1 || matchIdx === state.activeItemIndex) + return { ...state, searchQuery }; + return { + ...state, + searchQuery, + activeItemIndex: matchIdx, + activationTrigger: 1 /* Other */ + }; + }, + [4 /* ClearSearch */](state) { + if (state.searchQuery === "") + return state; + return { ...state, searchQuery: "", searchActiveItemIndex: null }; + }, + [5 /* RegisterItem */]: (state, action) => { + let adjustedState = adjustOrderedState3(state, (items) => [ + ...items, + { id: action.id, dataRef: action.dataRef } + ]); + return { ...state, ...adjustedState }; + }, + [6 /* UnregisterItem */]: (state, action) => { + let adjustedState = adjustOrderedState3(state, (items) => { + let idx = items.findIndex((a) => a.id === action.id); + if (idx !== -1) + items.splice(idx, 1); + return items; + }); + return { + ...state, + ...adjustedState, + activationTrigger: 1 /* Other */ + }; + } +}; +var MenuContext = (0, import_react36.createContext)(null); +MenuContext.displayName = "MenuContext"; +function useMenuContext(component) { + let context = (0, import_react36.useContext)(MenuContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, useMenuContext); + throw err; + } + return context; +} +function stateReducer5(state, action) { + return match(action.type, reducers5, state, action); +} +var DEFAULT_MENU_TAG = import_react36.Fragment; +function MenuFn(props, ref) { + let { __demoMode = false, ...theirProps } = props; + let reducerBag = (0, import_react36.useReducer)(stateReducer5, { + __demoMode, + menuState: __demoMode ? 0 /* Open */ : 1 /* Closed */, + buttonRef: (0, import_react36.createRef)(), + itemsRef: (0, import_react36.createRef)(), + items: [], + searchQuery: "", + activeItemIndex: null, + activationTrigger: 1 /* Other */ + }); + let [{ menuState, itemsRef, buttonRef }, dispatch] = reducerBag; + let menuRef = useSyncRefs(ref); + useOutsideClick( + [buttonRef, itemsRef], + (event, target) => { + var _a3; + dispatch({ type: 1 /* CloseMenu */ }); + if (!isFocusableElement(target, 1 /* Loose */)) { + event.preventDefault(); + (_a3 = buttonRef.current) == null ? void 0 : _a3.focus(); + } + }, + menuState === 0 /* Open */ + ); + let close = useEvent(() => { + dispatch({ type: 1 /* CloseMenu */ }); + }); + let slot = (0, import_react36.useMemo)( + () => ({ open: menuState === 0 /* Open */, close }), + [menuState, close] + ); + let ourProps = { ref: menuRef }; + return /* @__PURE__ */ import_react36.default.createElement(MenuContext.Provider, { value: reducerBag }, /* @__PURE__ */ import_react36.default.createElement( + OpenClosedProvider, + { + value: match(menuState, { + [0 /* Open */]: 1 /* Open */, + [1 /* Closed */]: 2 /* Closed */ + }) + }, + render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_MENU_TAG, + name: "Menu" + }) + )); +} +var DEFAULT_BUTTON_TAG4 = "button"; +function ButtonFn4(props, ref) { + var _a3; + let internalId = useId(); + let { id = `headlessui-menu-button-${internalId}`, ...theirProps } = props; + let [state, dispatch] = useMenuContext("Menu.Button"); + let buttonRef = useSyncRefs(state.buttonRef, ref); + let d = useDisposables(); + let handleKeyDown = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + case "ArrowDown" /* ArrowDown */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 0 /* OpenMenu */ }); + d.nextFrame(() => dispatch({ type: 2 /* GoToItem */, focus: 0 /* First */ })); + break; + case "ArrowUp" /* ArrowUp */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 0 /* OpenMenu */ }); + d.nextFrame(() => dispatch({ type: 2 /* GoToItem */, focus: 3 /* Last */ })); + break; + } + }); + let handleKeyUp = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + event.preventDefault(); + break; + } + }); + let handleClick = useEvent((event) => { + if (isDisabledReactIssue7711(event.currentTarget)) + return event.preventDefault(); + if (props.disabled) + return; + if (state.menuState === 0 /* Open */) { + dispatch({ type: 1 /* CloseMenu */ }); + d.nextFrame(() => { + var _a4; + return (_a4 = state.buttonRef.current) == null ? void 0 : _a4.focus({ preventScroll: true }); + }); + } else { + event.preventDefault(); + dispatch({ type: 0 /* OpenMenu */ }); + } + }); + let slot = (0, import_react36.useMemo)( + () => ({ open: state.menuState === 0 /* Open */ }), + [state] + ); + let ourProps = { + ref: buttonRef, + id, + type: useResolveButtonType(props, state.buttonRef), + "aria-haspopup": "menu", + "aria-controls": (_a3 = state.itemsRef.current) == null ? void 0 : _a3.id, + "aria-expanded": props.disabled ? void 0 : state.menuState === 0 /* Open */, + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG4, + name: "Menu.Button" + }); +} +var DEFAULT_ITEMS_TAG = "div"; +var ItemsRenderFeatures = 1 /* RenderStrategy */ | 2 /* Static */; +function ItemsFn(props, ref) { + var _a3, _b; + let internalId = useId(); + let { id = `headlessui-menu-items-${internalId}`, ...theirProps } = props; + let [state, dispatch] = useMenuContext("Menu.Items"); + let itemsRef = useSyncRefs(state.itemsRef, ref); + let ownerDocument = useOwnerDocument(state.itemsRef); + let searchDisposables = useDisposables(); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return state.menuState === 0 /* Open */; + })(); + (0, import_react36.useEffect)(() => { + let container = state.itemsRef.current; + if (!container) + return; + if (state.menuState !== 0 /* Open */) + return; + if (container === (ownerDocument == null ? void 0 : ownerDocument.activeElement)) + return; + container.focus({ preventScroll: true }); + }, [state.menuState, state.itemsRef, ownerDocument]); + useTreeWalker({ + container: state.itemsRef.current, + enabled: state.menuState === 0 /* Open */, + accept(node) { + if (node.getAttribute("role") === "menuitem") + return NodeFilter.FILTER_REJECT; + if (node.hasAttribute("role")) + return NodeFilter.FILTER_SKIP; + return NodeFilter.FILTER_ACCEPT; + }, + walk(node) { + node.setAttribute("role", "none"); + } + }); + let handleKeyDown = useEvent((event) => { + var _a4, _b2; + searchDisposables.dispose(); + switch (event.key) { + case " " /* Space */: + if (state.searchQuery !== "") { + event.preventDefault(); + event.stopPropagation(); + return dispatch({ type: 3 /* Search */, value: event.key }); + } + case "Enter" /* Enter */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 1 /* CloseMenu */ }); + if (state.activeItemIndex !== null) { + let { dataRef } = state.items[state.activeItemIndex]; + (_b2 = (_a4 = dataRef.current) == null ? void 0 : _a4.domRef.current) == null ? void 0 : _b2.click(); + } + restoreFocusIfNecessary(state.buttonRef.current); + break; + case "ArrowDown" /* ArrowDown */: + event.preventDefault(); + event.stopPropagation(); + return dispatch({ type: 2 /* GoToItem */, focus: 2 /* Next */ }); + case "ArrowUp" /* ArrowUp */: + event.preventDefault(); + event.stopPropagation(); + return dispatch({ type: 2 /* GoToItem */, focus: 1 /* Previous */ }); + case "Home" /* Home */: + case "PageUp" /* PageUp */: + event.preventDefault(); + event.stopPropagation(); + return dispatch({ type: 2 /* GoToItem */, focus: 0 /* First */ }); + case "End" /* End */: + case "PageDown" /* PageDown */: + event.preventDefault(); + event.stopPropagation(); + return dispatch({ type: 2 /* GoToItem */, focus: 3 /* Last */ }); + case "Escape" /* Escape */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 1 /* CloseMenu */ }); + disposables().nextFrame(() => { + var _a5; + return (_a5 = state.buttonRef.current) == null ? void 0 : _a5.focus({ preventScroll: true }); + }); + break; + case "Tab" /* Tab */: + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 1 /* CloseMenu */ }); + disposables().nextFrame(() => { + focusFrom( + state.buttonRef.current, + event.shiftKey ? 2 /* Previous */ : 4 /* Next */ + ); + }); + break; + default: + if (event.key.length === 1) { + dispatch({ type: 3 /* Search */, value: event.key }); + searchDisposables.setTimeout(() => dispatch({ type: 4 /* ClearSearch */ }), 350); + } + break; + } + }); + let handleKeyUp = useEvent((event) => { + switch (event.key) { + case " " /* Space */: + event.preventDefault(); + break; + } + }); + let slot = (0, import_react36.useMemo)( + () => ({ open: state.menuState === 0 /* Open */ }), + [state] + ); + let ourProps = { + "aria-activedescendant": state.activeItemIndex === null ? void 0 : (_a3 = state.items[state.activeItemIndex]) == null ? void 0 : _a3.id, + "aria-labelledby": (_b = state.buttonRef.current) == null ? void 0 : _b.id, + id, + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + role: "menu", + tabIndex: 0, + ref: itemsRef + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_ITEMS_TAG, + features: ItemsRenderFeatures, + visible, + name: "Menu.Items" + }); +} +var DEFAULT_ITEM_TAG = import_react36.Fragment; +function ItemFn(props, ref) { + let internalId = useId(); + let { id = `headlessui-menu-item-${internalId}`, disabled = false, ...theirProps } = props; + let [state, dispatch] = useMenuContext("Menu.Item"); + let active = state.activeItemIndex !== null ? state.items[state.activeItemIndex].id === id : false; + let internalItemRef = (0, import_react36.useRef)(null); + let itemRef = useSyncRefs(ref, internalItemRef); + useIsoMorphicEffect(() => { + if (state.__demoMode) + return; + if (state.menuState !== 0 /* Open */) + return; + if (!active) + return; + if (state.activationTrigger === 0 /* Pointer */) + return; + let d = disposables(); + d.requestAnimationFrame(() => { + var _a3, _b; + (_b = (_a3 = internalItemRef.current) == null ? void 0 : _a3.scrollIntoView) == null ? void 0 : _b.call(_a3, { block: "nearest" }); + }); + return d.dispose; + }, [ + state.__demoMode, + internalItemRef, + active, + state.menuState, + state.activationTrigger, + /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ + state.activeItemIndex + ]); + let getTextValue2 = useTextValue(internalItemRef); + let bag = (0, import_react36.useRef)({ + disabled, + domRef: internalItemRef, + get textValue() { + return getTextValue2(); + } + }); + useIsoMorphicEffect(() => { + bag.current.disabled = disabled; + }, [bag, disabled]); + useIsoMorphicEffect(() => { + dispatch({ type: 5 /* RegisterItem */, id, dataRef: bag }); + return () => dispatch({ type: 6 /* UnregisterItem */, id }); + }, [bag, id]); + let close = useEvent(() => { + dispatch({ type: 1 /* CloseMenu */ }); + }); + let handleClick = useEvent((event) => { + if (disabled) + return event.preventDefault(); + dispatch({ type: 1 /* CloseMenu */ }); + restoreFocusIfNecessary(state.buttonRef.current); + }); + let handleFocus = useEvent(() => { + if (disabled) + return dispatch({ type: 2 /* GoToItem */, focus: 5 /* Nothing */ }); + dispatch({ type: 2 /* GoToItem */, focus: 4 /* Specific */, id }); + }); + let pointer = useTrackedPointer(); + let handleEnter = useEvent((evt) => pointer.update(evt)); + let handleMove = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (active) + return; + dispatch({ + type: 2 /* GoToItem */, + focus: 4 /* Specific */, + id, + trigger: 0 /* Pointer */ + }); + }); + let handleLeave = useEvent((evt) => { + if (!pointer.wasMoved(evt)) + return; + if (disabled) + return; + if (!active) + return; + dispatch({ type: 2 /* GoToItem */, focus: 5 /* Nothing */ }); + }); + let slot = (0, import_react36.useMemo)( + () => ({ active, disabled, close }), + [active, disabled, close] + ); + let ourProps = { + id, + ref: itemRef, + role: "menuitem", + tabIndex: disabled === true ? void 0 : -1, + "aria-disabled": disabled === true ? true : void 0, + disabled: void 0, + // Never forward the `disabled` prop + onClick: handleClick, + onFocus: handleFocus, + onPointerEnter: handleEnter, + onMouseEnter: handleEnter, + onPointerMove: handleMove, + onMouseMove: handleMove, + onPointerLeave: handleLeave, + onMouseLeave: handleLeave + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_ITEM_TAG, + name: "Menu.Item" + }); +} +var MenuRoot = forwardRefWithAs(MenuFn); +var Button4 = forwardRefWithAs(ButtonFn4); +var Items = forwardRefWithAs(ItemsFn); +var Item = forwardRefWithAs(ItemFn); +var Menu = Object.assign(MenuRoot, { Button: Button4, Items, Item }); + +// src/components/popover/popover.tsx +var import_react37 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var reducers6 = { + [0 /* TogglePopover */]: (state) => { + let nextState = { + ...state, + popoverState: match(state.popoverState, { + [0 /* Open */]: 1 /* Closed */, + [1 /* Closed */]: 0 /* Open */ + }) + }; + if (nextState.popoverState === 0 /* Open */) { + nextState.__demoMode = false; + } + return nextState; + }, + [1 /* ClosePopover */](state) { + if (state.popoverState === 1 /* Closed */) + return state; + return { ...state, popoverState: 1 /* Closed */ }; + }, + [2 /* SetButton */](state, action) { + if (state.button === action.button) + return state; + return { ...state, button: action.button }; + }, + [3 /* SetButtonId */](state, action) { + if (state.buttonId === action.buttonId) + return state; + return { ...state, buttonId: action.buttonId }; + }, + [4 /* SetPanel */](state, action) { + if (state.panel === action.panel) + return state; + return { ...state, panel: action.panel }; + }, + [5 /* SetPanelId */](state, action) { + if (state.panelId === action.panelId) + return state; + return { ...state, panelId: action.panelId }; + } +}; +var PopoverContext = (0, import_react37.createContext)(null); +PopoverContext.displayName = "PopoverContext"; +function usePopoverContext(component) { + let context = (0, import_react37.useContext)(PopoverContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, usePopoverContext); + throw err; + } + return context; +} +var PopoverAPIContext = (0, import_react37.createContext)(null); +PopoverAPIContext.displayName = "PopoverAPIContext"; +function usePopoverAPIContext(component) { + let context = (0, import_react37.useContext)(PopoverAPIContext); + if (context === null) { + let err = new Error(`<${component} /> is missing a parent component.`); + if (Error.captureStackTrace) + Error.captureStackTrace(err, usePopoverAPIContext); + throw err; + } + return context; +} +var PopoverGroupContext = (0, import_react37.createContext)(null); +PopoverGroupContext.displayName = "PopoverGroupContext"; +function usePopoverGroupContext() { + return (0, import_react37.useContext)(PopoverGroupContext); +} +var PopoverPanelContext = (0, import_react37.createContext)(null); +PopoverPanelContext.displayName = "PopoverPanelContext"; +function usePopoverPanelContext() { + return (0, import_react37.useContext)(PopoverPanelContext); +} +function stateReducer6(state, action) { + return match(action.type, reducers6, state, action); +} +var DEFAULT_POPOVER_TAG = "div"; +function PopoverFn(props, ref) { + var _a3; + let { __demoMode = false, ...theirProps } = props; + let internalPopoverRef = (0, import_react37.useRef)(null); + let popoverRef = useSyncRefs( + ref, + optionalRef((ref2) => { + internalPopoverRef.current = ref2; + }) + ); + let buttons = (0, import_react37.useRef)([]); + let reducerBag = (0, import_react37.useReducer)(stateReducer6, { + __demoMode, + popoverState: __demoMode ? 0 /* Open */ : 1 /* Closed */, + buttons, + button: null, + buttonId: null, + panel: null, + panelId: null, + beforePanelSentinel: (0, import_react37.createRef)(), + afterPanelSentinel: (0, import_react37.createRef)() + }); + let [ + { popoverState, button, buttonId, panel, panelId, beforePanelSentinel, afterPanelSentinel }, + dispatch + ] = reducerBag; + let ownerDocument = useOwnerDocument((_a3 = internalPopoverRef.current) != null ? _a3 : button); + let isPortalled = (0, import_react37.useMemo)(() => { + if (!button) + return false; + if (!panel) + return false; + for (let root2 of document.querySelectorAll("body > *")) { + if (Number(root2 == null ? void 0 : root2.contains(button)) ^ Number(root2 == null ? void 0 : root2.contains(panel))) { + return true; + } + } + let elements = getFocusableElements(); + let buttonIdx = elements.indexOf(button); + let beforeIdx = (buttonIdx + elements.length - 1) % elements.length; + let afterIdx = (buttonIdx + 1) % elements.length; + let beforeElement = elements[beforeIdx]; + let afterElement = elements[afterIdx]; + if (!panel.contains(beforeElement) && !panel.contains(afterElement)) { + return true; + } + return false; + }, [button, panel]); + let buttonIdRef = useLatestValue(buttonId); + let panelIdRef = useLatestValue(panelId); + let registerBag = (0, import_react37.useMemo)( + () => ({ + buttonId: buttonIdRef, + panelId: panelIdRef, + close: () => dispatch({ type: 1 /* ClosePopover */ }) + }), + [buttonIdRef, panelIdRef, dispatch] + ); + let groupContext = usePopoverGroupContext(); + let registerPopover = groupContext == null ? void 0 : groupContext.registerPopover; + let isFocusWithinPopoverGroup = useEvent(() => { + var _a4; + return (_a4 = groupContext == null ? void 0 : groupContext.isFocusWithinPopoverGroup()) != null ? _a4 : (ownerDocument == null ? void 0 : ownerDocument.activeElement) && ((button == null ? void 0 : button.contains(ownerDocument.activeElement)) || (panel == null ? void 0 : panel.contains(ownerDocument.activeElement))); + }); + (0, import_react37.useEffect)(() => registerPopover == null ? void 0 : registerPopover(registerBag), [registerPopover, registerBag]); + let [portals, PortalWrapper] = useNestedPortals(); + let root = useRootContainers({ + portals, + defaultContainers: [button, panel] + }); + useEventListener( + ownerDocument == null ? void 0 : ownerDocument.defaultView, + "focus", + (event) => { + var _a4, _b, _c, _d; + if (event.target === window) + return; + if (!(event.target instanceof HTMLElement)) + return; + if (popoverState !== 0 /* Open */) + return; + if (isFocusWithinPopoverGroup()) + return; + if (!button) + return; + if (!panel) + return; + if (root.contains(event.target)) + return; + if ((_b = (_a4 = beforePanelSentinel.current) == null ? void 0 : _a4.contains) == null ? void 0 : _b.call(_a4, event.target)) + return; + if ((_d = (_c = afterPanelSentinel.current) == null ? void 0 : _c.contains) == null ? void 0 : _d.call(_c, event.target)) + return; + dispatch({ type: 1 /* ClosePopover */ }); + }, + true + ); + useOutsideClick( + root.resolveContainers, + (event, target) => { + dispatch({ type: 1 /* ClosePopover */ }); + if (!isFocusableElement(target, 1 /* Loose */)) { + event.preventDefault(); + button == null ? void 0 : button.focus(); + } + }, + popoverState === 0 /* Open */ + ); + let close = useEvent( + (focusableElement) => { + dispatch({ type: 1 /* ClosePopover */ }); + let restoreElement = (() => { + if (!focusableElement) + return button; + if (focusableElement instanceof HTMLElement) + return focusableElement; + if ("current" in focusableElement && focusableElement.current instanceof HTMLElement) + return focusableElement.current; + return button; + })(); + restoreElement == null ? void 0 : restoreElement.focus(); + } + ); + let api = (0, import_react37.useMemo)( + () => ({ close, isPortalled }), + [close, isPortalled] + ); + let slot = (0, import_react37.useMemo)( + () => ({ open: popoverState === 0 /* Open */, close }), + [popoverState, close] + ); + let ourProps = { ref: popoverRef }; + return /* @__PURE__ */ import_react37.default.createElement(PopoverPanelContext.Provider, { value: null }, /* @__PURE__ */ import_react37.default.createElement(PopoverContext.Provider, { value: reducerBag }, /* @__PURE__ */ import_react37.default.createElement(PopoverAPIContext.Provider, { value: api }, /* @__PURE__ */ import_react37.default.createElement( + OpenClosedProvider, + { + value: match(popoverState, { + [0 /* Open */]: 1 /* Open */, + [1 /* Closed */]: 2 /* Closed */ + }) + }, + /* @__PURE__ */ import_react37.default.createElement(PortalWrapper, null, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_POPOVER_TAG, + name: "Popover" + }), /* @__PURE__ */ import_react37.default.createElement(root.MainTreeNode, null)) + )))); +} +var DEFAULT_BUTTON_TAG5 = "button"; +function ButtonFn5(props, ref) { + let internalId = useId(); + let { id = `headlessui-popover-button-${internalId}`, ...theirProps } = props; + let [state, dispatch] = usePopoverContext("Popover.Button"); + let { isPortalled } = usePopoverAPIContext("Popover.Button"); + let internalButtonRef = (0, import_react37.useRef)(null); + let sentinelId = `headlessui-focus-sentinel-${useId()}`; + let groupContext = usePopoverGroupContext(); + let closeOthers = groupContext == null ? void 0 : groupContext.closeOthers; + let panelContext = usePopoverPanelContext(); + let isWithinPanel = panelContext !== null; + (0, import_react37.useEffect)(() => { + if (isWithinPanel) + return; + dispatch({ type: 3 /* SetButtonId */, buttonId: id }); + return () => { + dispatch({ type: 3 /* SetButtonId */, buttonId: null }); + }; + }, [isWithinPanel, id, dispatch]); + let [uniqueIdentifier] = (0, import_react37.useState)(() => Symbol()); + let buttonRef = useSyncRefs( + internalButtonRef, + ref, + isWithinPanel ? null : (button) => { + if (button) { + state.buttons.current.push(uniqueIdentifier); + } else { + let idx = state.buttons.current.indexOf(uniqueIdentifier); + if (idx !== -1) + state.buttons.current.splice(idx, 1); + } + if (state.buttons.current.length > 1) { + console.warn( + "You are already using a but only 1 is supported." + ); + } + button && dispatch({ type: 2 /* SetButton */, button }); + } + ); + let withinPanelButtonRef = useSyncRefs(internalButtonRef, ref); + let ownerDocument = useOwnerDocument(internalButtonRef); + let handleKeyDown = useEvent((event) => { + var _a3, _b, _c; + if (isWithinPanel) { + if (state.popoverState === 1 /* Closed */) + return; + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + event.preventDefault(); + (_b = (_a3 = event.target).click) == null ? void 0 : _b.call(_a3); + dispatch({ type: 1 /* ClosePopover */ }); + (_c = state.button) == null ? void 0 : _c.focus(); + break; + } + } else { + switch (event.key) { + case " " /* Space */: + case "Enter" /* Enter */: + event.preventDefault(); + event.stopPropagation(); + if (state.popoverState === 1 /* Closed */) + closeOthers == null ? void 0 : closeOthers(state.buttonId); + dispatch({ type: 0 /* TogglePopover */ }); + break; + case "Escape" /* Escape */: + if (state.popoverState !== 0 /* Open */) + return closeOthers == null ? void 0 : closeOthers(state.buttonId); + if (!internalButtonRef.current) + return; + if ((ownerDocument == null ? void 0 : ownerDocument.activeElement) && !internalButtonRef.current.contains(ownerDocument.activeElement)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 1 /* ClosePopover */ }); + break; + } + } + }); + let handleKeyUp = useEvent((event) => { + if (isWithinPanel) + return; + if (event.key === " " /* Space */) { + event.preventDefault(); + } + }); + let handleClick = useEvent((event) => { + var _a3, _b; + if (isDisabledReactIssue7711(event.currentTarget)) + return; + if (props.disabled) + return; + if (isWithinPanel) { + dispatch({ type: 1 /* ClosePopover */ }); + (_a3 = state.button) == null ? void 0 : _a3.focus(); + } else { + event.preventDefault(); + event.stopPropagation(); + if (state.popoverState === 1 /* Closed */) + closeOthers == null ? void 0 : closeOthers(state.buttonId); + dispatch({ type: 0 /* TogglePopover */ }); + (_b = state.button) == null ? void 0 : _b.focus(); + } + }); + let handleMouseDown = useEvent((event) => { + event.preventDefault(); + event.stopPropagation(); + }); + let visible = state.popoverState === 0 /* Open */; + let slot = (0, import_react37.useMemo)(() => ({ open: visible }), [visible]); + let type = useResolveButtonType(props, internalButtonRef); + let ourProps = isWithinPanel ? { + ref: withinPanelButtonRef, + type, + onKeyDown: handleKeyDown, + onClick: handleClick + } : { + ref: buttonRef, + id: state.buttonId, + type, + "aria-expanded": props.disabled ? void 0 : state.popoverState === 0 /* Open */, + "aria-controls": state.panel ? state.panelId : void 0, + onKeyDown: handleKeyDown, + onKeyUp: handleKeyUp, + onClick: handleClick, + onMouseDown: handleMouseDown + }; + let direction = useTabDirection(); + let handleFocus = useEvent(() => { + let el = state.panel; + if (!el) + return; + function run() { + let result = match(direction.current, { + [0 /* Forwards */]: () => focusIn(el, 1 /* First */), + [1 /* Backwards */]: () => focusIn(el, 8 /* Last */) + }); + if (result === 0 /* Error */) { + focusIn( + getFocusableElements().filter((el2) => el2.dataset.headlessuiFocusGuard !== "true"), + match(direction.current, { + [0 /* Forwards */]: 4 /* Next */, + [1 /* Backwards */]: 2 /* Previous */ + }), + { relativeTo: state.button } + ); + } + } + if (false) {} else { + run(); + } + }); + return /* @__PURE__ */ import_react37.default.createElement(import_react37.default.Fragment, null, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_BUTTON_TAG5, + name: "Popover.Button" + }), visible && !isWithinPanel && isPortalled && /* @__PURE__ */ import_react37.default.createElement( + Hidden, + { + id: sentinelId, + features: 2 /* Focusable */, + "data-headlessui-focus-guard": true, + as: "button", + type: "button", + onFocus: handleFocus + } + )); +} +var DEFAULT_OVERLAY_TAG2 = "div"; +var OverlayRenderFeatures = 1 /* RenderStrategy */ | 2 /* Static */; +function OverlayFn2(props, ref) { + let internalId = useId(); + let { id = `headlessui-popover-overlay-${internalId}`, ...theirProps } = props; + let [{ popoverState }, dispatch] = usePopoverContext("Popover.Overlay"); + let overlayRef = useSyncRefs(ref); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return popoverState === 0 /* Open */; + })(); + let handleClick = useEvent((event) => { + if (isDisabledReactIssue7711(event.currentTarget)) + return event.preventDefault(); + dispatch({ type: 1 /* ClosePopover */ }); + }); + let slot = (0, import_react37.useMemo)( + () => ({ open: popoverState === 0 /* Open */ }), + [popoverState] + ); + let ourProps = { + ref: overlayRef, + id, + "aria-hidden": true, + onClick: handleClick + }; + return render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_OVERLAY_TAG2, + features: OverlayRenderFeatures, + visible, + name: "Popover.Overlay" + }); +} +var DEFAULT_PANEL_TAG3 = "div"; +var PanelRenderFeatures2 = 1 /* RenderStrategy */ | 2 /* Static */; +function PanelFn3(props, ref) { + let internalId = useId(); + let { id = `headlessui-popover-panel-${internalId}`, focus = false, ...theirProps } = props; + let [state, dispatch] = usePopoverContext("Popover.Panel"); + let { close, isPortalled } = usePopoverAPIContext("Popover.Panel"); + let beforePanelSentinelId = `headlessui-focus-sentinel-before-${useId()}`; + let afterPanelSentinelId = `headlessui-focus-sentinel-after-${useId()}`; + let internalPanelRef = (0, import_react37.useRef)(null); + let panelRef = useSyncRefs(internalPanelRef, ref, (panel) => { + dispatch({ type: 4 /* SetPanel */, panel }); + }); + let ownerDocument = useOwnerDocument(internalPanelRef); + useIsoMorphicEffect(() => { + dispatch({ type: 5 /* SetPanelId */, panelId: id }); + return () => { + dispatch({ type: 5 /* SetPanelId */, panelId: null }); + }; + }, [id, dispatch]); + let usesOpenClosedState = useOpenClosed(); + let visible = (() => { + if (usesOpenClosedState !== null) { + return (usesOpenClosedState & 1 /* Open */) === 1 /* Open */; + } + return state.popoverState === 0 /* Open */; + })(); + let handleKeyDown = useEvent((event) => { + var _a3; + switch (event.key) { + case "Escape" /* Escape */: + if (state.popoverState !== 0 /* Open */) + return; + if (!internalPanelRef.current) + return; + if ((ownerDocument == null ? void 0 : ownerDocument.activeElement) && !internalPanelRef.current.contains(ownerDocument.activeElement)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + dispatch({ type: 1 /* ClosePopover */ }); + (_a3 = state.button) == null ? void 0 : _a3.focus(); + break; + } + }); + (0, import_react37.useEffect)(() => { + var _a3; + if (props.static) + return; + if (state.popoverState === 1 /* Closed */ && ((_a3 = props.unmount) != null ? _a3 : true)) { + dispatch({ type: 4 /* SetPanel */, panel: null }); + } + }, [state.popoverState, props.unmount, props.static, dispatch]); + (0, import_react37.useEffect)(() => { + if (state.__demoMode) + return; + if (!focus) + return; + if (state.popoverState !== 0 /* Open */) + return; + if (!internalPanelRef.current) + return; + let activeElement = ownerDocument == null ? void 0 : ownerDocument.activeElement; + if (internalPanelRef.current.contains(activeElement)) + return; + focusIn(internalPanelRef.current, 1 /* First */); + }, [state.__demoMode, focus, internalPanelRef, state.popoverState]); + let slot = (0, import_react37.useMemo)( + () => ({ open: state.popoverState === 0 /* Open */, close }), + [state, close] + ); + let ourProps = { + ref: panelRef, + id, + onKeyDown: handleKeyDown, + onBlur: focus && state.popoverState === 0 /* Open */ ? (event) => { + var _a3, _b, _c, _d, _e; + let el = event.relatedTarget; + if (!el) + return; + if (!internalPanelRef.current) + return; + if ((_a3 = internalPanelRef.current) == null ? void 0 : _a3.contains(el)) + return; + dispatch({ type: 1 /* ClosePopover */ }); + if (((_c = (_b = state.beforePanelSentinel.current) == null ? void 0 : _b.contains) == null ? void 0 : _c.call(_b, el)) || ((_e = (_d = state.afterPanelSentinel.current) == null ? void 0 : _d.contains) == null ? void 0 : _e.call(_d, el))) { + el.focus({ preventScroll: true }); + } + } : void 0, + tabIndex: -1 + }; + let direction = useTabDirection(); + let handleBeforeFocus = useEvent(() => { + let el = internalPanelRef.current; + if (!el) + return; + function run() { + match(direction.current, { + [0 /* Forwards */]: () => { + var _a3; + let result = focusIn(el, 1 /* First */); + if (result === 0 /* Error */) { + (_a3 = state.afterPanelSentinel.current) == null ? void 0 : _a3.focus(); + } + }, + [1 /* Backwards */]: () => { + var _a3; + (_a3 = state.button) == null ? void 0 : _a3.focus({ preventScroll: true }); + } + }); + } + if (false) {} else { + run(); + } + }); + let handleAfterFocus = useEvent(() => { + let el = internalPanelRef.current; + if (!el) + return; + function run() { + match(direction.current, { + [0 /* Forwards */]: () => { + var _a3; + if (!state.button) + return; + let elements = getFocusableElements(); + let idx = elements.indexOf(state.button); + let before = elements.slice(0, idx + 1); + let after = elements.slice(idx + 1); + let combined = [...after, ...before]; + for (let element of combined.slice()) { + if (element.dataset.headlessuiFocusGuard === "true" || ((_a3 = state.panel) == null ? void 0 : _a3.contains(element))) { + let idx2 = combined.indexOf(element); + if (idx2 !== -1) + combined.splice(idx2, 1); + } + } + focusIn(combined, 1 /* First */, { sorted: false }); + }, + [1 /* Backwards */]: () => { + var _a3; + let result = focusIn(el, 2 /* Previous */); + if (result === 0 /* Error */) { + (_a3 = state.button) == null ? void 0 : _a3.focus(); + } + } + }); + } + if (false) {} else { + run(); + } + }); + return /* @__PURE__ */ import_react37.default.createElement(PopoverPanelContext.Provider, { value: id }, visible && isPortalled && /* @__PURE__ */ import_react37.default.createElement( + Hidden, + { + id: beforePanelSentinelId, + ref: state.beforePanelSentinel, + features: 2 /* Focusable */, + "data-headlessui-focus-guard": true, + as: "button", + type: "button", + onFocus: handleBeforeFocus + } + ), render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_PANEL_TAG3, + features: PanelRenderFeatures2, + visible, + name: "Popover.Panel" + }), visible && isPortalled && /* @__PURE__ */ import_react37.default.createElement( + Hidden, + { + id: afterPanelSentinelId, + ref: state.afterPanelSentinel, + features: 2 /* Focusable */, + "data-headlessui-focus-guard": true, + as: "button", + type: "button", + onFocus: handleAfterFocus + } + )); +} +var DEFAULT_GROUP_TAG2 = "div"; +function GroupFn2(props, ref) { + let internalGroupRef = (0, import_react37.useRef)(null); + let groupRef = useSyncRefs(internalGroupRef, ref); + let [popovers, setPopovers] = (0, import_react37.useState)([]); + let unregisterPopover = useEvent((registerbag) => { + setPopovers((existing) => { + let idx = existing.indexOf(registerbag); + if (idx !== -1) { + let clone = existing.slice(); + clone.splice(idx, 1); + return clone; + } + return existing; + }); + }); + let registerPopover = useEvent((registerbag) => { + setPopovers((existing) => [...existing, registerbag]); + return () => unregisterPopover(registerbag); + }); + let isFocusWithinPopoverGroup = useEvent(() => { + var _a3; + let ownerDocument = getOwnerDocument(internalGroupRef); + if (!ownerDocument) + return false; + let element = ownerDocument.activeElement; + if ((_a3 = internalGroupRef.current) == null ? void 0 : _a3.contains(element)) + return true; + return popovers.some((bag) => { + var _a4, _b; + return ((_a4 = ownerDocument.getElementById(bag.buttonId.current)) == null ? void 0 : _a4.contains(element)) || ((_b = ownerDocument.getElementById(bag.panelId.current)) == null ? void 0 : _b.contains(element)); + }); + }); + let closeOthers = useEvent((buttonId) => { + for (let popover of popovers) { + if (popover.buttonId.current !== buttonId) + popover.close(); + } + }); + let contextBag = (0, import_react37.useMemo)( + () => ({ + registerPopover, + unregisterPopover, + isFocusWithinPopoverGroup, + closeOthers + }), + [registerPopover, unregisterPopover, isFocusWithinPopoverGroup, closeOthers] + ); + let slot = (0, import_react37.useMemo)(() => ({}), []); + let theirProps = props; + let ourProps = { ref: groupRef }; + return /* @__PURE__ */ import_react37.default.createElement(PopoverGroupContext.Provider, { value: contextBag }, render({ + ourProps, + theirProps, + slot, + defaultTag: DEFAULT_GROUP_TAG2, + name: "Popover.Group" + })); +} +var PopoverRoot = forwardRefWithAs(PopoverFn); +var Button5 = forwardRefWithAs(ButtonFn5); +var Overlay2 = forwardRefWithAs(OverlayFn2); +var Panel3 = forwardRefWithAs(PanelFn3); +var Group2 = forwardRefWithAs(GroupFn2); +var Popover = Object.assign(PopoverRoot, { Button: Button5, Overlay: Overlay2, Panel: Panel3, Group: Group2 }); + +// src/components/radio-group/radio-group.tsx +var import_react40 = __toESM(__webpack_require__(/*! react */ "react"), 1); + +// src/hooks/use-flags.ts +var import_react38 = __webpack_require__(/*! react */ "react"); +function useFlags(initialFlags = 0) { + let [flags, setFlags] = (0, import_react38.useState)(initialFlags); + let mounted = useIsMounted(); + let addFlag = (0, import_react38.useCallback)( + (flag) => { + if (!mounted.current) + return; + setFlags((flags2) => flags2 | flag); + }, + [flags, mounted] + ); + let hasFlag = (0, import_react38.useCallback)((flag) => Boolean(flags & flag), [flags]); + let removeFlag = (0, import_react38.useCallback)( + (flag) => { + if (!mounted.current) + return; + setFlags((flags2) => flags2 & ~flag); + }, + [setFlags, mounted] + ); + let toggleFlag = (0, import_react38.useCallback)( + (flag) => { + if (!mounted.current) + return; + setFlags((flags2) => flags2 ^ flag); + }, + [setFlags] + ); + return { flags, addFlag, hasFlag, removeFlag, toggleFlag }; +} + +// src/components/label/label.tsx +var import_react39 = __toESM(__webpack_require__(/*! react */ "react"), 1); +var LabelContext = (0, import_react39.createContext)( + null +); +function useLabelContext() { + let context = (0, import_react39.useContext)(LabelContext); + if (context === null) { + let err = new Error("You used a
*/ private string|\Stringable|null $securityPostDenormalize = null, - private array|string|null $types = null, + array|string|null $types = null, /* * The related php types. */ @@ -203,11 +214,12 @@ public function __construct( private ?bool $genId = null, private ?string $uriTemplate = null, private ?string $property = null, + private ?string $policy = null, + array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null, private array $extraProperties = [], ) { - if (\is_string($types)) { - $this->types = (array) $types; - } + $this->types = \is_string($types) ? (array) $types : $types; + $this->serialize = \is_array($serialize) ? $serialize : (array) $serialize; } public function getProperty(): ?string @@ -585,4 +597,33 @@ public function withUriTemplate(?string $uriTemplate): self return $metadata; } + + public function getPolicy(): ?string + { + return $this->policy; + } + + public function withPolicy(?string $policy): static + { + $self = clone $this; + $self->policy = $policy; + + return $self; + } + + public function getSerialize(): ?array + { + return $this->serialize; + } + + /** + * @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array $serialize + */ + public function withSerialize(array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth $serialize): static + { + $self = clone $this; + $self->serialize = (array) $serialize; + + return $self; + } } diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index 3084756294d..d46dda2ced4 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -252,7 +252,7 @@ public function __construct( * ```yaml * # api/config/api_platform/resources.yaml * App\Entity\Book: - * urlGenerationStrategy: !php/const ApiPlatform\Api\UrlGeneratorInterface::ABS_URL + * urlGenerationStrategy: !php/const ApiPlatform\Metadata\UrlGeneratorInterface::ABS_URL * ``` * * ```xml @@ -321,7 +321,6 @@ public function __construct( protected ?array $denormalizationContext = null, protected ?bool $collectDenormalizationErrors = null, protected ?array $hydraContext = null, - protected ?array $openapiContext = null, // TODO Remove in 4.0 protected bool|OpenApiOperation|null $openapi = null, /** * The `validationContext` option configures the context of validation for the current ApiResource. @@ -960,6 +959,9 @@ public function __construct( $provider = null, $processor = null, protected ?OptionsInterface $stateOptions = null, + protected mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array|Parameters|null $parameters = null, protected array $extraProperties = [], ) { @@ -1002,6 +1004,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties ); @@ -1327,29 +1332,6 @@ public function withHydraContext(array $hydraContext): self return $self; } - /** - * TODO Remove in 4.0. - * - * @deprecated - */ - public function getOpenapiContext(): ?array - { - return $this->openapiContext; - } - - /** - * TODO Remove in 4.0. - * - * @deprecated - */ - public function withOpenapiContext(array $openapiContext): self - { - $self = clone $this; - $self->openapiContext = $openapiContext; - - return $self; - } - public function getOpenapi(): bool|OpenApiOperation|null { return $this->openapi; diff --git a/src/Metadata/Delete.php b/src/Metadata/Delete.php index 78635f4539b..1656e928cf0 100644 --- a/src/Metadata/Delete.php +++ b/src/Metadata/Delete.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], ) { parent::__construct( @@ -123,7 +125,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -171,6 +172,9 @@ class: $class, name: $name, provider: $provider, processor: $processor, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties, collectDenormalizationErrors: $collectDenormalizationErrors, parameters: $parameters, diff --git a/src/Metadata/Error.php b/src/Metadata/Error.php index 1be8995f779..84a652c0f1e 100644 --- a/src/Metadata/Error.php +++ b/src/Metadata/Error.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -121,7 +120,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, diff --git a/src/Metadata/ErrorResource.php b/src/Metadata/ErrorResource.php index 2e2c7a981da..a10676037e6 100644 --- a/src/Metadata/ErrorResource.php +++ b/src/Metadata/ErrorResource.php @@ -49,7 +49,6 @@ public function __construct( ?array $denormalizationContext = null, ?bool $collectDenormalizationErrors = null, ?array $hydraContext = null, - ?array $openapiContext = null, OpenApiOperation|bool|null $openapi = null, ?array $validationContext = null, ?array $filters = null, @@ -117,7 +116,6 @@ class: $class, denormalizationContext: $denormalizationContext, collectDenormalizationErrors: $collectDenormalizationErrors, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, validationContext: $validationContext, filters: $filters, diff --git a/src/Exception/ErrorCodeSerializableInterface.php b/src/Metadata/Exception/ErrorCodeSerializableInterface.php similarity index 90% rename from src/Exception/ErrorCodeSerializableInterface.php rename to src/Metadata/Exception/ErrorCodeSerializableInterface.php index 5a3550c8c3a..f46ad0f7305 100644 --- a/src/Exception/ErrorCodeSerializableInterface.php +++ b/src/Metadata/Exception/ErrorCodeSerializableInterface.php @@ -11,12 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Exception; +namespace ApiPlatform\Metadata\Exception; /** * An exception which has a serializable application-specific error code. - * - * @deprecated */ interface ErrorCodeSerializableInterface { diff --git a/src/Metadata/Exception/InvalidArgumentException.php b/src/Metadata/Exception/InvalidArgumentException.php index d407409dc26..d81b9a8fa4e 100644 --- a/src/Metadata/Exception/InvalidArgumentException.php +++ b/src/Metadata/Exception/InvalidArgumentException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\InvalidArgumentException as LegacyInvalidArgumentException; - -if (class_exists(LegacyInvalidArgumentException::class)) { - class InvalidArgumentException extends LegacyInvalidArgumentException - { - } -} else { - /** - * Invalid argument exception. - * - * @author Kévin Dunglas - */ - class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface - { - } +/** + * Invalid argument exception. + * + * @author Kévin Dunglas + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/InvalidIdentifierException.php b/src/Metadata/Exception/InvalidIdentifierException.php index 1a23925f29f..bfebcc59e5a 100644 --- a/src/Metadata/Exception/InvalidIdentifierException.php +++ b/src/Metadata/Exception/InvalidIdentifierException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\InvalidIdentifierException as LegacyInvalidIdentifierException; - -if (class_exists(LegacyInvalidIdentifierException::class)) { - class InvalidIdentifierException extends LegacyInvalidIdentifierException - { - } -} else { - /** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - */ - final class InvalidIdentifierException extends \Exception implements ExceptionInterface - { - } +/** + * Identifier is not valid exception. + * + * @author Antoine Bluchet + */ +final class InvalidIdentifierException extends \Exception implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/InvalidUriVariableException.php b/src/Metadata/Exception/InvalidUriVariableException.php index 17bfede8a0e..a35cf2a3280 100644 --- a/src/Metadata/Exception/InvalidUriVariableException.php +++ b/src/Metadata/Exception/InvalidUriVariableException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\InvalidUriVariableException as LegacyInvalidUriVariableException; - -if (class_exists(LegacyInvalidUriVariableException::class)) { - class InvalidUriVariableException extends LegacyInvalidUriVariableException - { - } -} else { - /** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - */ - final class InvalidUriVariableException extends \Exception implements ExceptionInterface - { - } +/** + * Identifier is not valid exception. + * + * @author Antoine Bluchet + */ +final class InvalidUriVariableException extends \Exception implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/ItemNotFoundException.php b/src/Metadata/Exception/ItemNotFoundException.php index 368664719f4..507d9f7ed88 100644 --- a/src/Metadata/Exception/ItemNotFoundException.php +++ b/src/Metadata/Exception/ItemNotFoundException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\ItemNotFoundException as LegacyItemNotFoundException; - -if (class_exists(LegacyItemNotFoundException::class)) { - class ItemNotFoundException extends LegacyItemNotFoundException - { - } -} else { - /** - * Item not found exception. - * - * @author Amrouche Hamza - */ - class ItemNotFoundException extends InvalidArgumentException - { - } +/** + * Item not found exception. + * + * @author Amrouche Hamza + */ +class ItemNotFoundException extends InvalidArgumentException +{ } diff --git a/src/Metadata/Exception/OperationNotFoundException.php b/src/Metadata/Exception/OperationNotFoundException.php index e7257b72a69..d98d3a74cd8 100644 --- a/src/Metadata/Exception/OperationNotFoundException.php +++ b/src/Metadata/Exception/OperationNotFoundException.php @@ -13,17 +13,9 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\OperationNotFoundException as LegacyOperationNotFoundException; - -if (class_exists(LegacyOperationNotFoundException::class)) { - class OperationNotFoundException extends LegacyOperationNotFoundException - { - } -} else { - /** - * Operation not found exception. - */ - class OperationNotFoundException extends \InvalidArgumentException implements ExceptionInterface - { - } +/** + * Operation not found exception. + */ +class OperationNotFoundException extends \InvalidArgumentException implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/PropertyNotFoundException.php b/src/Metadata/Exception/PropertyNotFoundException.php index 5efe3df6409..8e3a208892a 100644 --- a/src/Metadata/Exception/PropertyNotFoundException.php +++ b/src/Metadata/Exception/PropertyNotFoundException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\PropertyNotFoundException as LegacyPropertyNotFoundException; - -if (class_exists(LegacyPropertyNotFoundException::class)) { - class PropertyNotFoundException extends LegacyPropertyNotFoundException - { - } -} else { - /** - * Property not found exception. - * - * @author Kévin Dunglas - */ - class PropertyNotFoundException extends \Exception implements ExceptionInterface - { - } +/** + * Property not found exception. + * + * @author Kévin Dunglas + */ +class PropertyNotFoundException extends \Exception implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/ResourceClassNotFoundException.php b/src/Metadata/Exception/ResourceClassNotFoundException.php index b1b83beaff6..f06f3951758 100644 --- a/src/Metadata/Exception/ResourceClassNotFoundException.php +++ b/src/Metadata/Exception/ResourceClassNotFoundException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\ResourceClassNotFoundException as LegacyResourceClassNotFoundException; - -if (class_exists(LegacyResourceClassNotFoundException::class)) { - class ResourceClassNotFoundException extends LegacyResourceClassNotFoundException - { - } -} else { - /** - * Resource class not found exception. - * - * @author Kévin Dunglas - */ - class ResourceClassNotFoundException extends \Exception implements ExceptionInterface - { - } +/** + * Resource class not found exception. + * + * @author Kévin Dunglas + */ +class ResourceClassNotFoundException extends \Exception implements ExceptionInterface +{ } diff --git a/src/Metadata/Exception/RuntimeException.php b/src/Metadata/Exception/RuntimeException.php index 3eb1a4987be..1333d53fabf 100644 --- a/src/Metadata/Exception/RuntimeException.php +++ b/src/Metadata/Exception/RuntimeException.php @@ -13,19 +13,11 @@ namespace ApiPlatform\Metadata\Exception; -use ApiPlatform\Exception\RuntimeException as LegacyRuntimeException; - -if (class_exists(LegacyRuntimeException::class)) { - class RuntimeException extends LegacyRuntimeException - { - } -} else { - /** - * Runtime exception. - * - * @author Kévin Dunglas - */ - class RuntimeException extends \RuntimeException implements ExceptionInterface - { - } +/** + * Runtime exception. + * + * @author Kévin Dunglas + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ } diff --git a/src/Exception/InvalidResourceException.php b/src/Metadata/Exception/StatusAwareExceptionInterface.php similarity index 55% rename from src/Exception/InvalidResourceException.php rename to src/Metadata/Exception/StatusAwareExceptionInterface.php index d1c0b56f72d..214c3d92ee9 100644 --- a/src/Exception/InvalidResourceException.php +++ b/src/Metadata/Exception/StatusAwareExceptionInterface.php @@ -11,15 +11,12 @@ declare(strict_types=1); -namespace ApiPlatform\Exception; +namespace ApiPlatform\Metadata\Exception; -/** - * Invalid resource exception. - * - * @author Paul Le Corre - * - * @deprecated - */ -class InvalidResourceException extends \Exception implements ExceptionInterface +interface StatusAwareExceptionInterface { + /** + * Sets the status code. + */ + public function setStatus(int $status): void; } diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 143c3b77250..9ec28bb382c 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -90,7 +90,6 @@ private function buildExtendedBase(\SimpleXMLElement $resource): array 'schemes' => $this->buildArrayValue($resource, 'scheme'), 'cacheHeaders' => $this->buildCacheHeaders($resource), 'hydraContext' => isset($resource->hydraContext->values) ? $this->buildValues($resource->hydraContext->values) : null, - 'openapiContext' => isset($resource->openapiContext->values) ? $this->buildValues($resource->openapiContext->values) : null, // TODO Remove in 4.0 'openapi' => $this->buildOpenapi($resource), 'paginationViaCursor' => $this->buildPaginationViaCursor($resource), 'exceptionToStatus' => $this->buildExceptionToStatus($resource), diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php index 8bbb03bed69..64a097e8904 100644 --- a/src/Metadata/Extractor/YamlResourceExtractor.php +++ b/src/Metadata/Extractor/YamlResourceExtractor.php @@ -110,7 +110,6 @@ private function buildExtendedBase(array $resource): array 'types' => $this->buildArrayValue($resource, 'types'), 'cacheHeaders' => $this->buildArrayValue($resource, 'cacheHeaders'), 'hydraContext' => $this->buildArrayValue($resource, 'hydraContext'), - 'openapiContext' => $this->buildArrayValue($resource, 'openapiContext'), // TODO Remove in 4.0 'openapi' => $this->buildOpenapi($resource), 'paginationViaCursor' => $this->buildArrayValue($resource, 'paginationViaCursor'), 'exceptionToStatus' => $this->buildArrayValue($resource, 'exceptionToStatus'), diff --git a/src/Metadata/Extractor/schema/resources.xsd b/src/Metadata/Extractor/schema/resources.xsd index 6ce6daa5c4b..5c90713d806 100644 --- a/src/Metadata/Extractor/schema/resources.xsd +++ b/src/Metadata/Extractor/schema/resources.xsd @@ -467,7 +467,6 @@ - diff --git a/src/Metadata/FilterInterface.php b/src/Metadata/FilterInterface.php index 5f393fa4b40..509ef4ed8eb 100644 --- a/src/Metadata/FilterInterface.php +++ b/src/Metadata/FilterInterface.php @@ -30,15 +30,14 @@ interface FilterInterface * - description : the description of the filter * - strategy: the used strategy * - is_collection: if this filter is for collection - * - swagger: deprecated, use openapi instead * - openapi: additional parameters for the path operation in the version 3 spec, - * e.g. 'openapi' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'schema' => [ + * e.g. 'openapi' => ApiPlatform\OpenApi\Model\Parameter( + * description: 'My Description', + * name: 'My Name', + * schema: [ * 'type' => 'integer', * ] - * ] + * ) * - schema: schema definition, * e.g. 'schema' => [ * 'type' => 'string', @@ -50,7 +49,7 @@ interface FilterInterface * * @param class-string $resourceClass * - * @return array, openapi?: array|\ApiPlatform\OpenApi\Model\Parameter, schema?: array}> + * @return array}> */ public function getDescription(string $resourceClass): array; } diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php index a1cc716c1ce..f636ca3d1a0 100644 --- a/src/Metadata/Get.php +++ b/src/Metadata/Get.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], ) { parent::__construct( @@ -122,7 +124,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -173,6 +174,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties, ); } diff --git a/src/Metadata/GetCollection.php b/src/Metadata/GetCollection.php index 0eb6c1b1c63..a5fd3f29e6a 100644 --- a/src/Metadata/GetCollection.php +++ b/src/Metadata/GetCollection.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + array|string|null $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], private ?string $itemUriTemplate = null, ) { @@ -123,7 +125,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -174,6 +175,9 @@ class: $class, processor: $processor, parameters: $parameters, extraProperties: $extraProperties, + rules: $rules, + policy: $policy, + middleware: $middleware, stateOptions: $stateOptions, ); } diff --git a/src/Metadata/GraphQl/Operation.php b/src/Metadata/GraphQl/Operation.php index 0e0fb6374ba..fb87283cb89 100644 --- a/src/Metadata/GraphQl/Operation.php +++ b/src/Metadata/GraphQl/Operation.php @@ -90,6 +90,8 @@ public function __construct( ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, ?bool $queryParameterValidationEnabled = null, + mixed $rules = null, + ?string $policy = null, array $extraProperties = [], ) { parent::__construct( @@ -139,6 +141,8 @@ class: $class, stateOptions: $stateOptions, parameters: $parameters, queryParameterValidationEnabled: $queryParameterValidationEnabled, + rules: $rules, + policy: $policy, extraProperties: $extraProperties ); } diff --git a/src/Metadata/GraphQl/Query.php b/src/Metadata/GraphQl/Query.php index 633341923a1..9d629425310 100644 --- a/src/Metadata/GraphQl/Query.php +++ b/src/Metadata/GraphQl/Query.php @@ -73,6 +73,8 @@ public function __construct( ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, ?bool $queryParameterValidationEnabled = null, + mixed $rules = null, + ?string $policy = null, array $extraProperties = [], protected ?bool $nested = null, @@ -130,6 +132,8 @@ class: $class, stateOptions: $stateOptions, parameters: $parameters, queryParameterValidationEnabled: $queryParameterValidationEnabled, + policy: $policy, + rules: $rules, extraProperties: $extraProperties ); } diff --git a/src/Metadata/GraphQl/QueryCollection.php b/src/Metadata/GraphQl/QueryCollection.php index 1ec1a8c7435..6a029a531ff 100644 --- a/src/Metadata/GraphQl/QueryCollection.php +++ b/src/Metadata/GraphQl/QueryCollection.php @@ -74,6 +74,8 @@ public function __construct( protected ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, ?bool $queryParameterValidationEnabled = null, + mixed $rules = null, + ?string $policy = null, array $extraProperties = [], ?bool $nested = null, @@ -130,6 +132,8 @@ class: $class, processor: $processor, parameters: $parameters, queryParameterValidationEnabled: $queryParameterValidationEnabled, + policy: $policy, + rules: $rules, extraProperties: $extraProperties, nested: $nested, ); diff --git a/src/Metadata/GraphQl/Subscription.php b/src/Metadata/GraphQl/Subscription.php index b989ee1ffb3..59a66fab0c7 100644 --- a/src/Metadata/GraphQl/Subscription.php +++ b/src/Metadata/GraphQl/Subscription.php @@ -73,6 +73,8 @@ public function __construct( ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, ?bool $queryParameterValidationEnabled = null, + mixed $rules = null, + ?string $policy = null, array $extraProperties = [], ) { parent::__construct( @@ -128,6 +130,8 @@ class: $class, stateOptions: $stateOptions, parameters: $parameters, queryParameterValidationEnabled: $queryParameterValidationEnabled, + policy: $policy, + rules: $rules, extraProperties: $extraProperties, ); } diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index b2ee2bbeb50..eadd07482b0 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -65,7 +65,6 @@ class HttpOperation extends Operation * @param array|null $normalizationContext {@see https://api-platform.com/docs/core/serialization/#using-serialization-groups} * @param array|null $denormalizationContext {@see https://api-platform.com/docs/core/serialization/#using-serialization-groups} * @param array|null $hydraContext {@see https://api-platform.com/docs/core/extending-jsonld-context/#hydra} - * @param array|null $openapiContext {@see https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts} * @param array{ * class?: string|null, * name?: string, @@ -152,7 +151,6 @@ public function __construct( protected ?array $cacheHeaders = null, protected ?array $paginationViaCursor = null, protected ?array $hydraContext = null, - protected ?array $openapiContext = null, // TODO Remove in 4.0 protected bool|OpenApiOperation|Webhook|null $openapi = null, protected ?array $exceptionToStatus = null, protected ?array $links = null, @@ -203,6 +201,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + array|string|null $rules = null, + ?string $policy = null, + array|string|null $middleware = null, ?bool $queryParameterValidationEnabled = null, array $extraProperties = [], ) { @@ -252,6 +253,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, queryParameterValidationEnabled: $queryParameterValidationEnabled, extraProperties: $extraProperties ); @@ -572,19 +576,6 @@ public function withHydraContext(array $hydraContext): self return $self; } - public function getOpenapiContext(): ?array - { - return $this->openapiContext; - } - - public function withOpenapiContext(array $openapiContext): self - { - $self = clone $this; - $self->openapiContext = $openapiContext; - - return $self; - } - public function getOpenapi(): bool|OpenApiOperation|Webhook|null { return $this->openapi; diff --git a/src/Metadata/IdentifiersExtractor.php b/src/Metadata/IdentifiersExtractor.php index d44736231d9..a9f5efe801e 100644 --- a/src/Metadata/IdentifiersExtractor.php +++ b/src/Metadata/IdentifiersExtractor.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Metadata; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -35,10 +34,7 @@ final class IdentifiersExtractor implements IdentifiersExtractorInterface use ResourceClassInfoTrait; private readonly PropertyAccessorInterface $propertyAccessor; - /** - * @param LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver - */ - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ?PropertyAccessorInterface $propertyAccessor = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ?PropertyAccessorInterface $propertyAccessor = null) { $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceClassResolver = $resourceClassResolver; diff --git a/src/Metadata/IdentifiersExtractorInterface.php b/src/Metadata/IdentifiersExtractorInterface.php index 5c4ef2d81f7..a8623a06119 100644 --- a/src/Metadata/IdentifiersExtractorInterface.php +++ b/src/Metadata/IdentifiersExtractorInterface.php @@ -15,30 +15,21 @@ use ApiPlatform\Metadata\Exception\RuntimeException; -if (interface_exists(\ApiPlatform\Api\IdentifiersExtractorInterface::class)) { - class_alias( - \ApiPlatform\Api\IdentifiersExtractorInterface::class, - __NAMESPACE__.'\IdentifiersExtractorInterface' - ); - - if (false) { // @phpstan-ignore-line - interface IdentifiersExtractorInterface extends \ApiPlatform\Api\IdentifiersExtractorInterface - { - } - } -} else { +/** + * Extracts identifiers for a given Resource according to the retrieved Metadata. + * + * @author Antoine Bluchet + */ +interface IdentifiersExtractorInterface +{ /** - * Extracts identifiers for a given Resource according to the retrieved Metadata. + * Finds identifiers from an Item (object). + * + * @param array $context + * + * @throws RuntimeException * - * @author Antoine Bluchet + * @return array */ - interface IdentifiersExtractorInterface - { - /** - * Finds identifiers from an Item (object). - * - * @throws RuntimeException - */ - public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array; - } + public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array; } diff --git a/src/Metadata/IriConverterInterface.php b/src/Metadata/IriConverterInterface.php index 7c16b2f729a..0d0b51dfa8c 100644 --- a/src/Metadata/IriConverterInterface.php +++ b/src/Metadata/IriConverterInterface.php @@ -17,41 +17,31 @@ use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Exception\RuntimeException; -if (interface_exists(\ApiPlatform\Api\IriConverterInterface::class)) { - class_alias( - \ApiPlatform\Api\IriConverterInterface::class, - __NAMESPACE__.'\IriConverterInterface' - ); - - if (false) { // @phpstan-ignore-line - interface IriConverterInterface extends \ApiPlatform\Api\IriConverterInterface - { - } - } -} else { +/** + * Converts item and resources to IRI and vice versa. + * + * @author Kévin Dunglas + */ +interface IriConverterInterface +{ /** - * Converts item and resources to IRI and vice versa. + * Retrieves an item from its IRI. + * + * @param array|array{request?: Request, resource_class?: string|class-string} $context * - * @author Kévin Dunglas + * @throws InvalidArgumentException + * @throws ItemNotFoundException */ - interface IriConverterInterface - { - /** - * Retrieves an item from its IRI. - * - * @throws InvalidArgumentException - * @throws ItemNotFoundException - */ - public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object; + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object; - /** - * Gets the IRI associated with the given item. - * - * @param object|class-string $resource - * - * @throws InvalidArgumentException - * @throws RuntimeException - */ - public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string; - } + /** + * Gets the IRI associated with the given item. + * + * @param object|class-string $resource + * @param array|array{force_resource_class?: string|class-string, item_uri_template?: string, uri_variables?: array} $context + * + * @throws InvalidArgumentException + * @throws RuntimeException + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string; } diff --git a/src/Metadata/JsonSchemaFilterInterface.php b/src/Metadata/JsonSchemaFilterInterface.php new file mode 100644 index 00000000000..3bad4a1f241 --- /dev/null +++ b/src/Metadata/JsonSchemaFilterInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata; + +interface JsonSchemaFilterInterface +{ + /** + * @return array + */ + public function getSchema(Parameter $parameter): array; +} diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index a2c85d1f893..75712b0ff6e 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -23,16 +23,17 @@ abstract class Metadata protected ?Parameters $parameters = null; /** - * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties - * @param string|\Stringable|null $security https://api-platform.com/docs/core/security - * @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization - * @param mixed|null $mercure - * @param mixed|null $messenger - * @param mixed|null $input - * @param mixed|null $output - * @param mixed|null $provider - * @param mixed|null $processor - * @param Parameters|array $parameters + * @param string|null $deprecationReason https://api-platform.com/docs/core/deprecations/#deprecating-resource-classes-operations-and-properties + * @param string|\Stringable|null $security https://api-platform.com/docs/core/security + * @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization + * @param mixed|null $mercure + * @param mixed|null $messenger + * @param mixed|null $input + * @param mixed|null $output + * @param mixed|null $provider + * @param mixed|null $processor + * @param Parameters|array $parameters + * @param callable|string|array $rules Laravel rules can be a FormRequest class, a callable or an array of rules */ public function __construct( protected ?string $shortName = null, @@ -76,6 +77,9 @@ public function __construct( * @experimental */ array|Parameters|null $parameters = null, + protected mixed $rules = null, + protected ?string $policy = null, + protected array|string|null $middleware = null, protected ?bool $queryParameterValidationEnabled = null, protected array $extraProperties = [], ) { @@ -579,6 +583,25 @@ public function withStateOptions(?OptionsInterface $stateOptions): static return $self; } + /** + * @return string|callable|array + */ + public function getRules(): mixed + { + return $this->rules; + } + + /** + * @param string|callable|array $rules + */ + public function withRules(mixed $rules): static + { + $self = clone $this; + $self->rules = $rules; + + return $self; + } + public function getParameters(): ?Parameters { return $this->parameters; @@ -605,6 +628,32 @@ public function withQueryParameterValidationEnabled(bool $queryParameterValidati return $self; } + public function getPolicy(): ?string + { + return $this->policy; + } + + public function withPolicy(string $policy): static + { + $self = clone $this; + $self->policy = $policy; + + return $self; + } + + public function getMiddleware(): mixed + { + return $this->middleware; + } + + public function withMiddleware(string|array $middleware): static + { + $self = clone $this; + $self->middleware = $middleware; + + return $self; + } + public function getExtraProperties(): ?array { return $this->extraProperties; diff --git a/src/Metadata/NotExposed.php b/src/Metadata/NotExposed.php index 84fcbe45a2c..5c609684c7d 100644 --- a/src/Metadata/NotExposed.php +++ b/src/Metadata/NotExposed.php @@ -56,7 +56,6 @@ public function __construct( ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = false, ?array $exceptionToStatus = null, @@ -135,7 +134,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, diff --git a/src/Metadata/OpenApiParameterFilterInterface.php b/src/Metadata/OpenApiParameterFilterInterface.php new file mode 100644 index 00000000000..9593785a19b --- /dev/null +++ b/src/Metadata/OpenApiParameterFilterInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata; + +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; + +interface OpenApiParameterFilterInterface +{ + /** + * @return OpenApiParameter|OpenApiParameter[]|null + */ + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null; +} diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index bfa35ac93c7..b48ff9827f2 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -807,6 +807,9 @@ public function __construct( protected $processor = null, protected ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + array|string|null $rules = null, + ?string $policy = null, + array|string|null $middleware = null, ?bool $queryParameterValidationEnabled = null, protected array $extraProperties = [], ) { @@ -849,6 +852,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, queryParameterValidationEnabled: $queryParameterValidationEnabled, extraProperties: $extraProperties, ); diff --git a/src/Metadata/Parameter.php b/src/Metadata/Parameter.php index 135899b03fe..aa91e5d762f 100644 --- a/src/Metadata/Parameter.php +++ b/src/Metadata/Parameter.php @@ -14,9 +14,9 @@ namespace ApiPlatform\Metadata; use ApiPlatform\OpenApi; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use ApiPlatform\State\ParameterNotFound; use ApiPlatform\State\ParameterProviderInterface; -use Symfony\Component\Validator\Constraint; /** * @experimental @@ -28,23 +28,24 @@ abstract class Parameter * @param array $extraProperties * @param ParameterProviderInterface|callable|string|null $provider * @param FilterInterface|string|null $filter - * @param Constraint|Constraint[]|null $constraints + * @param mixed $constraints an array of Symfony constraints, or an array of Laravel rules */ public function __construct( protected ?string $key = null, protected ?array $schema = null, - protected OpenApi\Model\Parameter|bool|null $openApi = null, // TODO: use false as type instead of bool + protected OpenApiParameter|array|false|null $openApi = null, protected mixed $provider = null, protected mixed $filter = null, protected ?string $property = null, protected ?string $description = null, protected ?bool $required = null, protected ?int $priority = null, - protected ?bool $hydra = null, - protected Constraint|array|null $constraints = null, + protected ?false $hydra = null, + protected mixed $constraints = null, protected string|\Stringable|null $security = null, protected ?string $securityMessage = null, protected ?array $extraProperties = [], + protected ?array $filterContext = null, ) { } @@ -61,7 +62,10 @@ public function getSchema(): ?array return $this->schema; } - public function getOpenApi(): OpenApi\Model\Parameter|bool|null + /** + * @return OpenApi\Model\Parameter[]|OpenApi\Model\Parameter|bool|null + */ + public function getOpenApi(): OpenApiParameter|array|bool|null { return $this->openApi; } @@ -101,10 +105,7 @@ public function getHydra(): ?bool return $this->hydra; } - /** - * @return Constraint|Constraint[]|null - */ - public function getConstraints(): Constraint|array|null + public function getConstraints(): mixed { return $this->constraints; } @@ -137,6 +138,11 @@ public function getExtraProperties(): array return $this->extraProperties; } + public function getFilterContext(): ?array + { + return $this->filterContext; + } + public function withKey(string $key): static { $self = clone $this; @@ -164,7 +170,10 @@ public function withSchema(array $schema): static return $self; } - public function withOpenApi(OpenApi\Model\Parameter $openApi): static + /** + * @param OpenApi\Model\Parameter[]|OpenApi\Model\Parameter|bool $openApi + */ + public function withOpenApi(OpenApiParameter|array|bool $openApi): static { $self = clone $this; $self->openApi = $openApi; @@ -218,7 +227,7 @@ public function withRequired(bool $required): static return $self; } - public function withHydra(bool $hydra): static + public function withHydra(false $hydra): static { $self = clone $this; $self->hydra = $hydra; @@ -226,7 +235,7 @@ public function withHydra(bool $hydra): static return $self; } - public function withConstraints(array|Constraint $constraints): static + public function withConstraints(mixed $constraints): static { $self = clone $this; $self->constraints = $constraints; diff --git a/src/Metadata/Patch.php b/src/Metadata/Patch.php index 956427148a2..17788a95acc 100644 --- a/src/Metadata/Patch.php +++ b/src/Metadata/Patch.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], ) { parent::__construct( @@ -123,7 +125,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -174,6 +175,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Post.php b/src/Metadata/Post.php index 3fee7ed362e..571a55c1056 100644 --- a/src/Metadata/Post.php +++ b/src/Metadata/Post.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], private ?string $itemUriTemplate = null, ) { @@ -124,7 +126,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -175,6 +176,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php b/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php index d9dbe14cb5d..3640ce671b6 100644 --- a/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/CachedPropertyMetadataFactory.php @@ -38,7 +38,7 @@ public function __construct(CacheItemPoolInterface $cacheItemPool, private reado */ public function create(string $resourceClass, string $property, array $options = []): ApiProperty { - $cacheKey = self::CACHE_KEY_PREFIX.md5(serialize([$resourceClass, $property, $options])); + $cacheKey = self::CACHE_KEY_PREFIX.hash('xxh3', serialize([$resourceClass, $property, $options])); return $this->getCached($cacheKey, fn (): ApiProperty => $this->decorated->create($resourceClass, $property, $options)); } diff --git a/src/Metadata/Property/Factory/CachedPropertyNameCollectionFactory.php b/src/Metadata/Property/Factory/CachedPropertyNameCollectionFactory.php index fca23f9e826..4a7f97092de 100644 --- a/src/Metadata/Property/Factory/CachedPropertyNameCollectionFactory.php +++ b/src/Metadata/Property/Factory/CachedPropertyNameCollectionFactory.php @@ -38,7 +38,7 @@ public function __construct(CacheItemPoolInterface $cacheItemPool, private reado */ public function create(string $resourceClass, array $options = []): PropertyNameCollection { - $cacheKey = self::CACHE_KEY_PREFIX.md5(serialize([$resourceClass, $options])); + $cacheKey = self::CACHE_KEY_PREFIX.hash('xxh3', serialize([$resourceClass, $options])); return $this->getCached($cacheKey, fn (): PropertyNameCollection => $this->decorated->create($resourceClass, $options)); } diff --git a/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php b/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php index 62e62a90384..3b2c473d24c 100644 --- a/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php +++ b/src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php @@ -93,7 +93,7 @@ private function handleNotFound(?ApiProperty $parentPropertyMetadata, string $re private function update(ApiProperty $propertyMetadata, array $metadata): ApiProperty { foreach (get_class_methods(ApiProperty::class) as $method) { - if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches) && null !== $val = $metadata[lcfirst($matches[1])]) { + if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches) && null !== ($val = $metadata[lcfirst($matches[1])] ?? null) && method_exists($propertyMetadata, "with{$matches[1]}")) { $propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val); } } diff --git a/src/Metadata/Property/Factory/PropertyMetadataFactoryInterface.php b/src/Metadata/Property/Factory/PropertyMetadataFactoryInterface.php index c1e76174975..b4532d1325f 100644 --- a/src/Metadata/Property/Factory/PropertyMetadataFactoryInterface.php +++ b/src/Metadata/Property/Factory/PropertyMetadataFactoryInterface.php @@ -26,6 +26,9 @@ interface PropertyMetadataFactoryInterface /** * Creates a property metadata. * + * @param array $options + * @param class-string|string $resourceClass + * * @throws PropertyNotFoundException */ public function create(string $resourceClass, string $property, array $options = []): ApiProperty; diff --git a/src/Metadata/Property/Factory/PropertyNameCollectionFactoryInterface.php b/src/Metadata/Property/Factory/PropertyNameCollectionFactoryInterface.php index b5d3e0c6ade..8d43b0bfef9 100644 --- a/src/Metadata/Property/Factory/PropertyNameCollectionFactoryInterface.php +++ b/src/Metadata/Property/Factory/PropertyNameCollectionFactoryInterface.php @@ -26,6 +26,8 @@ interface PropertyNameCollectionFactoryInterface /** * Creates the property name collection for the given class and options. * + * @param array $options + * * @throws ResourceClassNotFoundException */ public function create(string $resourceClass, array $options = []): PropertyNameCollection; diff --git a/src/Metadata/Put.php b/src/Metadata/Put.php index 9056130b216..a424da02fb9 100644 --- a/src/Metadata/Put.php +++ b/src/Metadata/Put.php @@ -44,7 +44,6 @@ public function __construct( ?array $cacheHeaders = null, ?array $paginationViaCursor = null, ?array $hydraContext = null, - ?array $openapiContext = null, bool|OpenApiOperation|Webhook|null $openapi = null, ?array $exceptionToStatus = null, ?bool $queryParameterValidationEnabled = null, @@ -96,6 +95,9 @@ public function __construct( $processor = null, ?OptionsInterface $stateOptions = null, array|Parameters|null $parameters = null, + mixed $rules = null, + ?string $policy = null, + array|string|null $middleware = null, array $extraProperties = [], private ?bool $allowCreate = null, ) { @@ -124,7 +126,6 @@ public function __construct( cacheHeaders: $cacheHeaders, paginationViaCursor: $paginationViaCursor, hydraContext: $hydraContext, - openapiContext: $openapiContext, openapi: $openapi, exceptionToStatus: $exceptionToStatus, queryParameterValidationEnabled: $queryParameterValidationEnabled, @@ -175,6 +176,9 @@ class: $class, processor: $processor, stateOptions: $stateOptions, parameters: $parameters, + rules: $rules, + policy: $policy, + middleware: $middleware, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 398d311b42b..2409ea3dda7 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Metadata\Resource\Factory; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -47,10 +46,11 @@ public function create(string $resourceClass): ResourceMetadataCollection } } - foreach ($this->buildResourceOperations($metadataCollection, $resourceClass) as $resource) { - $resourceMetadataCollection[] = $resource; + $resultCollection = new ResourceMetadataCollection($resourceClass); + foreach ($this->buildResourceOperations($metadataCollection, $resourceClass, iterator_to_array($resourceMetadataCollection)) as $resource) { + $resultCollection[] = $resource; } - return $resourceMetadataCollection; + return $resultCollection; } } diff --git a/src/Metadata/Resource/Factory/CachedResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/CachedResourceMetadataCollectionFactory.php index 523d9d6ebd2..88ffb94a9de 100644 --- a/src/Metadata/Resource/Factory/CachedResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/CachedResourceMetadataCollectionFactory.php @@ -36,7 +36,7 @@ public function __construct(private readonly CacheItemPoolInterface $cacheItemPo */ public function create(string $resourceClass): ResourceMetadataCollection { - $cacheKey = self::CACHE_KEY_PREFIX.md5($resourceClass); + $cacheKey = self::CACHE_KEY_PREFIX.hash('xxh3', $resourceClass); if (\array_key_exists($cacheKey, $this->localCache)) { return new ResourceMetadataCollection($resourceClass, $this->localCache[$cacheKey]); } diff --git a/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php deleted file mode 100644 index fe035530310..00000000000 --- a/src/Metadata/Resource/Factory/DeprecationResourceMetadataCollectionFactory.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Metadata\Resource\Factory; - -use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; - -/** - * Triggers resource deprecations. - * - * @final - * - * @internal - */ -class DeprecationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface -{ - // Hashmap to avoid triggering too many deprecations - private array $deprecated; - - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated) - { - } - - public function create(string $resourceClass): ResourceMetadataCollection - { - $resourceMetadataCollection = $this->decorated->create($resourceClass); - - foreach ($resourceMetadataCollection as $i => $resourceMetadata) { - $newOperations = []; - foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - $extraProperties = $operation->getExtraProperties(); - if ($operation instanceof Put && null === ($extraProperties['standard_put'] ?? null)) { - $this->triggerDeprecationOnce($operation, 'extraProperties["standard_put"]', 'In API Platform 4 PUT will always replace the data, use extraProperties["standard_put"] to "true" on every operation to avoid breaking PUT\'s behavior. Use PATCH to use the old behavior.'); - } - - if (true === ($extraProperties['use_legacy_parameter_validator'] ?? null)) { - $this->triggerDeprecationOnce($operation, 'extraProperties["use_legacy_parameter_validator"]', 'In API Platform 4 the query_parameter_validator will be removed in favor of Parameter constraints, set "use_legacy_parameter_validator" to false.'); - } - - $newOperations[$operationName] = $operation; - } - - $resourceMetadataCollection[$i] = $resourceMetadata->withOperations(new Operations($newOperations)); - } - - return $resourceMetadataCollection; - } - - private function triggerDeprecationOnce(Operation $operation, string $deprecationName, string $deprecationReason): void - { - if (isset($this->deprecated[$operation->getClass().$deprecationName])) { - return; - } - - $this->deprecated[$operation->getClass().$deprecationName] = true; - - trigger_deprecation('api-platform/core', '3.1', $deprecationReason); - } -} diff --git a/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php index 4070884317d..b090fddfc12 100644 --- a/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FormatsResourceMetadataCollectionFactory.php @@ -33,8 +33,12 @@ */ final class FormatsResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly array $formats, private readonly array $patchFormats, private readonly ?array $errorFormats = null) - { + public function __construct( + private readonly ResourceMetadataCollectionFactoryInterface $decorated, + private readonly array $formats, + private readonly array $patchFormats, + private readonly ?array $errorFormats = null, + ) { } /** diff --git a/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php index d2f3afcae72..dd5107f08d8 100644 --- a/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php @@ -23,7 +23,7 @@ */ final class LinkResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly LinkFactoryInterface $linkFactory, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null) + public function __construct(private readonly LinkFactoryInterface $linkFactory, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, private bool $graphQlEnabled = false) { } @@ -37,6 +37,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } + if (!$this->graphQlEnabled) { + return $resourceMetadataCollection; + } + foreach ($resourceMetadataCollection as $i => $resource) { $graphQlOperations = []; foreach ($resource->getGraphQlOperations() ?? [] as $graphQlOperation) { diff --git a/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php b/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php index e54d71f64ed..7561858e799 100644 --- a/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php +++ b/src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php @@ -55,7 +55,6 @@ private function isResourceMetadata(string $name): bool * Get * Post * Resource - * Put * Get * In the future, we will be able to use nested attributes (https://wiki.php.net/rfc/new_in_initializers). * @@ -63,10 +62,9 @@ private function isResourceMetadata(string $name): bool * * @return ApiResource[] */ - private function buildResourceOperations(array $metadataCollection, string $resourceClass): array + private function buildResourceOperations(array $metadataCollection, string $resourceClass, array $resources = []): array { $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; - $resources = []; $index = -1; $operationPriority = 0; $hasApiResource = false; @@ -143,11 +141,11 @@ private function buildResourceOperations(array $metadataCollection, string $reso $resources[$index] = $resource = $resource->withOperations(new Operations($operations)); // @phpstan-ignore-line } - $graphQlOperations = $resource->getGraphQlOperations(); if (!$this->graphQlEnabled) { continue; } + $graphQlOperations = $resource->getGraphQlOperations(); if (null === $graphQlOperations) { if (!$hasApiResource) { $resources[$index] = $resources[$index]->withGraphQlOperations([]); diff --git a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php index 33870c4b40c..0c35d28486c 100644 --- a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php @@ -20,7 +20,7 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; /** - * Adds a {@see NotExposed} operation with {@see NotFoundAction} on a resource which only has a GetCollection. + * Adds a {@see ApiPlatform\Metadata\NotExposed} operation with {@see ApiPlatform\Symfony\Action\NotFoundAction} on a resource which only has a GetCollection. * This operation helps to generate resource IRI for items. * * @author Vincent Chalamon diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php index dab37b23dae..6afe9a47634 100644 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -30,7 +30,6 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Util\CamelCaseToSnakeCaseNameConverter; use ApiPlatform\State\CreateProvider; use Psr\Log\LoggerInterface; @@ -67,7 +66,7 @@ private function addGlobalDefaults(ApiResource|Operation $operation): ApiResourc $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); } - if (null !== $currentValue) { + if (null !== $currentValue || null === $value) { continue; } @@ -96,7 +95,13 @@ private function getDefaultHttpOperations($resource): iterable $operations = []; foreach ($defaultOperations as $defaultOperation) { - $operations[] = new $defaultOperation(); + $operation = new $defaultOperation(); + + if ($operation instanceof Post && $resource->getUriTemplate() && !$resource->getProvider()) { + $operation = $operation->withProvider(CreateProvider::class); + } + + $operations[] = $operation; } return new Operations($operations); @@ -107,7 +112,7 @@ private function getDefaultHttpOperations($resource): iterable $post = $post->withProvider(CreateProvider::class); } - return [new Get(), new GetCollection(), $post, new Put(), new Patch(), new Delete()]; + return [new Get(), new GetCollection(), $post, new Patch(), new Delete()]; } private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php index 12a89a50fb9..066244428d1 100644 --- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php @@ -13,32 +13,22 @@ namespace ApiPlatform\Metadata\Resource\Factory; -use ApiPlatform\Doctrine\Odm\State\Options as DoctrineOdmOptions; -use ApiPlatform\Doctrine\Orm\State\Options as DoctrineOrmOptions; +use ApiPlatform\Doctrine\Odm\State\Options as DoctrineODMOptions; +use ApiPlatform\Doctrine\Orm\State\Options as DoctrineORMOptions; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\FilterInterface; -use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Parameter; use ApiPlatform\Metadata\Parameters; -use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; use Psr\Container\ContainerInterface; -use Symfony\Component\Validator\Constraints\Choice; -use Symfony\Component\Validator\Constraints\Count; -use Symfony\Component\Validator\Constraints\DivisibleBy; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; -use Symfony\Component\Validator\Constraints\Length; -use Symfony\Component\Validator\Constraints\LessThan; -use Symfony\Component\Validator\Constraints\LessThanOrEqual; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Validator\Constraints\Regex; -use Symfony\Component\Validator\Constraints\Type; -use Symfony\Component\Validator\Constraints\Unique; -use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Prepares Parameters documentation by reading its filter details and declaring an OpenApi parameter. @@ -47,8 +37,15 @@ */ final class ParameterResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, private readonly ?ContainerInterface $filterLocator = null) - { + private array $localPropertyCache; + + public function __construct( + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + private readonly ?ContainerInterface $filterLocator = null, + private readonly ?NameConverterInterface $nameConverter = null, + ) { } public function create(string $resourceClass): ResourceMetadataCollection @@ -56,24 +53,11 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); foreach ($resourceMetadataCollection as $i => $resource) { - $resourceClass = $resource->getClass(); $operations = $resource->getOperations(); $internalPriority = -1; foreach ($operations as $operationName => $operation) { - $parameters = $operation->getParameters() ?? new Parameters(); - foreach ($parameters as $key => $parameter) { - $key = $parameter->getKey() ?? $key; - $parameter = $this->setDefaults($key, $parameter, $resourceClass, $operation); - $priority = $parameter->getPriority() ?? $internalPriority--; - $parameters->add($key, $parameter->withPriority($priority)); - } - - // As we deprecate the parameter validator, we declare a parameter for each filter transfering validation to the new system - if ($operation->getFilters() && 0 === $parameters->count() && false === ($operation->getExtraProperties()['use_legacy_parameter_validator'] ?? true)) { - $parameters = $this->addFilterValidation($operation); - } - + $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority); if (\count($parameters) > 0) { $operations->add($operationName, $operation->withParameters($parameters)); } @@ -87,15 +71,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $internalPriority = -1; foreach ($graphQlOperations as $operationName => $operation) { - $parameters = $operation->getParameters() ?? new Parameters(); - foreach ($operation->getParameters() ?? [] as $key => $parameter) { - $key = $parameter->getKey() ?? $key; - $parameter = $this->setDefaults($key, $parameter, $resourceClass, $operation); - $priority = $parameter->getPriority() ?? $internalPriority--; - $parameters->add($key, $parameter->withPriority($priority)); + $parameters = $this->getDefaultParameters($operation, $resourceClass, $internalPriority); + if (\count($parameters) > 0) { + $graphQlOperations[$operationName] = $operation->withParameters($parameters); } - - $graphQlOperations[$operationName] = $operation->withParameters($parameters); } $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); @@ -104,183 +83,141 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } - private function setDefaults(string $key, Parameter $parameter, string $resourceClass, Operation $operation): Parameter + /** + * @return array{propertyNames: string[], properties: array} + */ + private function getProperties(string $resourceClass): array { - if (null === $parameter->getKey()) { - $parameter = $parameter->withKey($key); + if (isset($this->localPropertyCache[$resourceClass])) { + return $this->localPropertyCache[$resourceClass]; } - $filter = $parameter->getFilter(); - if (\is_string($filter) && $this->filterLocator->has($filter)) { - $filter = $this->filterLocator->get($filter); + $propertyNames = []; + $properties = []; + foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); + if ($propertyMetadata->isReadable()) { + $propertyNames[] = $property; + $properties[$property] = $propertyMetadata; + } } - if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) { - $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider'); - } + $this->localPropertyCache = [$resourceClass => ['propertyNames' => $propertyNames, 'properties' => $properties]]; - // Read filter description to populate the Parameter - $description = $filter instanceof FilterInterface ? $filter->getDescription($this->getFilterClass($operation)) : []; - if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) { - $parameter = $parameter->withSchema($schema); - } + return $this->localPropertyCache[$resourceClass]; + } - if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) { - $parameter = $parameter->withRequired($required); - } + private function getDefaultParameters(Operation $operation, string $resourceClass, int &$internalPriority): Parameters + { + ['propertyNames' => $propertyNames, 'properties' => $properties] = $this->getProperties($resourceClass); + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($parameters as $key => $parameter) { + if (':property' === $key) { + foreach ($propertyNames as $property) { + $converted = $this->nameConverter?->denormalize($property) ?? $property; + $propertyParameter = $this->setDefaults($converted, $parameter, $resourceClass, $properties, $operation); + $priority = $propertyParameter->getPriority() ?? $internalPriority--; + $parameters->add($converted, $propertyParameter->withPriority($priority)->withKey($converted)); + } - $schema = $parameter->getSchema() ?? (($openApi = $parameter->getOpenApi()) ? $openApi->getSchema() : null); + $parameters->remove($key, $parameter::class); + continue; + } - // Only add validation if the Symfony Validator is installed - if (interface_exists(ValidatorInterface::class) && !$parameter->getConstraints()) { - $parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi() ?: null); - } + $key = $parameter->getKey() ?? $key; - return $parameter; - } + if (str_contains($key, ':property')) { + $p = []; + foreach ($propertyNames as $prop) { + $p[$this->nameConverter?->denormalize($prop) ?? $prop] = $prop; + } - private function addSchemaValidation(Parameter $parameter, ?array $schema = null, bool $required = false, ?OpenApiParameter $openApi = null): Parameter - { - $assertions = []; + $parameter = $parameter->withExtraProperties($parameter->getExtraProperties() + ['_properties' => $p]); + } - if ($required && false !== ($allowEmptyValue = $openApi?->getAllowEmptyValue())) { - $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey())); + $parameter = $this->setDefaults($key, $parameter, $resourceClass, $properties, $operation); + $priority = $parameter->getPriority() ?? $internalPriority--; + $parameters->add($key, $parameter->withPriority($priority)); } - if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) { - $assertions[] = new NotBlank(allowNull: !$required); - } + return $parameters; + } - if (isset($schema['exclusiveMinimum'])) { - $assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']); + private function addFilterMetadata(Parameter $parameter): Parameter + { + if (!($filterId = $parameter->getFilter())) { + return $parameter; } - if (isset($schema['exclusiveMaximum'])) { - $assertions[] = new LessThan(value: $schema['exclusiveMaximum']); - } + $filter = \is_object($filterId) ? $filterId : $this->filterLocator->get($filterId); - if (isset($schema['minimum'])) { - $assertions[] = new GreaterThanOrEqual(value: $schema['minimum']); + if (!$filter) { + return $parameter; } - if (isset($schema['maximum'])) { - $assertions[] = new LessThanOrEqual(value: $schema['maximum']); + if (null === $parameter->getSchema() && $filter instanceof JsonSchemaFilterInterface && $schema = $filter->getSchema($parameter)) { + $parameter = $parameter->withSchema($schema); } - if (isset($schema['pattern'])) { - $assertions[] = new Regex($schema['pattern']); + if (null === $parameter->getOpenApi() && $filter instanceof OpenApiParameterFilterInterface && ($openApiParameter = $filter->getOpenApiParameters($parameter)) && $openApiParameter instanceof OpenApiParameter) { + $parameter = $parameter->withOpenApi($openApiParameter); } - if (isset($schema['maxLength']) || isset($schema['minLength'])) { - $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null); - } + return $parameter; + } - if (isset($schema['minItems']) || isset($schema['maxItems'])) { - $assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null); + /** + * @param array $properties + */ + private function setDefaults(string $key, Parameter $parameter, string $resourceClass, array $properties, Operation $operation): Parameter + { + if (null === $parameter->getKey()) { + $parameter = $parameter->withKey($key); } - if (isset($schema['multipleOf'])) { - $assertions[] = new DivisibleBy(value: $schema['multipleOf']); + $filter = $parameter->getFilter(); + if (\is_string($filter) && $this->filterLocator->has($filter)) { + $filter = $this->filterLocator->get($filter); } - if ($schema['uniqueItems'] ?? false) { - $assertions[] = new Unique(); + if ($filter instanceof SerializerFilterInterface && null === $parameter->getProvider()) { + $parameter = $parameter->withProvider('api_platform.serializer.filter_parameter_provider'); } - if (isset($schema['enum'])) { - $assertions[] = new Choice(choices: $schema['enum']); + // Read filter description to populate the Parameter + $description = $filter instanceof FilterInterface ? $filter->getDescription($this->getFilterClass($operation)) : []; + if (($schema = $description[$key]['schema'] ?? null) && null === $parameter->getSchema()) { + $parameter = $parameter->withSchema($schema); } - if (isset($schema['type']) && 'array' === $schema['type']) { - $assertions[] = new Type(type: 'array'); + $currentKey = $key; + if (null === $parameter->getProperty() && isset($properties[$key])) { + $parameter = $parameter->withProperty($key); } - if (!$assertions) { - return $parameter; + if (null === $parameter->getProperty() && $this->nameConverter && ($nameConvertedKey = $this->nameConverter->normalize($key)) && isset($properties[$nameConvertedKey])) { + $parameter = $parameter->withProperty($key)->withExtraProperties(['_query_property' => $nameConvertedKey] + $parameter->getExtraProperties()); + $currentKey = $nameConvertedKey; } - if (1 === \count($assertions)) { - return $parameter->withConstraints($assertions[0]); + if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) { + $parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties()); } - return $parameter->withConstraints($assertions); - } - - private function addFilterValidation(HttpOperation $operation): Parameters - { - $parameters = new Parameters(); - $internalPriority = -1; - - foreach ($operation->getFilters() as $filter) { - if (!$this->filterLocator->has($filter)) { - continue; - } - - $filter = $this->filterLocator->get($filter); - foreach ($filter->getDescription($this->getFilterClass($operation)) as $parameterName => $definition) { - $key = $parameterName; - $required = $definition['required'] ?? false; - $schema = $definition['schema'] ?? null; - - if (isset($definition['swagger'])) { - trigger_deprecation('api-platform/core', '3.4', 'The key "swagger" in a filter description is deprecated, use "schema" or "openapi" instead.'); - $schema = $schema ?? $definition['swagger']; - } - - $openApi = null; - if (isset($definition['openapi'])) { - trigger_deprecation('api-platform/core', '3.4', \sprintf('The key "openapi" in a filter description should be a "%s" class or use "schema" to specify the JSON Schema.', OpenApiParameter::class)); - if ($definition['openapi'] instanceof OpenApiParameter) { - $openApi = $definition['openapi']; - } else { - $schema = $schema ?? $openApi; - } - } - - if (isset($schema['allowEmptyValue']) && !$openApi) { - trigger_deprecation('api-platform/core', '3.4', 'The "allowEmptyValue" option should be declared using an "openapi" parameter.'); - $openApi = new OpenApiParameter(name: $key, in: 'query', allowEmptyValue: $schema['allowEmptyValue']); - } - - // The query parameter validator forced this, lets maintain BC on filters - if (true === $required && !$openApi) { - $openApi = new OpenApiParameter(name: $key, in: 'query', allowEmptyValue: false); - } - - if (\is_bool($schema['exclusiveMinimum'] ?? null)) { - trigger_deprecation('api-platform/core', '3.4', 'The "exclusiveMinimum" schema value should be a number not a boolean.'); - $schema['exclusiveMinimum'] = $schema['minimum']; - unset($schema['minimum']); - } - - if (\is_bool($schema['exclusiveMaximum'] ?? null)) { - trigger_deprecation('api-platform/core', '3.4', 'The "exclusiveMaximum" schema value should be a number not a boolean.'); - $schema['exclusiveMaximum'] = $schema['maximum']; - unset($schema['maximum']); - } - - $parameters->add($key, $this->addSchemaValidation( - // we disable openapi and hydra on purpose as their docs comes from filters see the condition for addFilterValidation above - new QueryParameter(key: $key, property: $definition['property'] ?? null, priority: $internalPriority--, schema: $schema, openApi: false, hydra: false), - $schema, - $required, - $openApi - )); - } + if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) { + $parameter = $parameter->withRequired($required); } - return $parameters; + return $this->addFilterMetadata($parameter); } private function getFilterClass(Operation $operation): ?string { $stateOptions = $operation->getStateOptions(); - - if ($stateOptions instanceof DoctrineOrmOptions) { + if ($stateOptions instanceof DoctrineORMOptions) { return $stateOptions->getEntityClass(); } - - if ($stateOptions instanceof DoctrineOdmOptions) { + if ($stateOptions instanceof DoctrineODMOptions) { return $stateOptions->getDocumentClass(); } diff --git a/src/Metadata/Resource/Factory/ResourceMetadataCollectionFactoryInterface.php b/src/Metadata/Resource/Factory/ResourceMetadataCollectionFactoryInterface.php index 53192947133..bc83e2229ce 100644 --- a/src/Metadata/Resource/Factory/ResourceMetadataCollectionFactoryInterface.php +++ b/src/Metadata/Resource/Factory/ResourceMetadataCollectionFactoryInterface.php @@ -26,6 +26,8 @@ interface ResourceMetadataCollectionFactoryInterface /** * Creates a resource metadata. * + * @param string|class-string $resourceClass + * * @throws ResourceClassNotFoundException */ public function create(string $resourceClass): ResourceMetadataCollection; diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index cfdf912cac5..2bc7a407a3f 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -29,8 +29,6 @@ final class UriTemplateResourceMetadataCollectionFactory implements ResourceMeta { use OperationDefaultsTrait; - private $triggerLegacyFormatOnce = []; - public function __construct(private readonly LinkFactoryInterface $linkFactory, private readonly PathSegmentNameGeneratorInterface $pathSegmentNameGenerator, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null) { } @@ -99,17 +97,11 @@ private function generateUriTemplate(HttpOperation $operation): string { $uriTemplate = $operation->getUriTemplate() ?? \sprintf('/%s', $this->pathSegmentNameGenerator->getSegmentName($operation->getShortName())); $uriVariables = $operation->getUriVariables() ?? []; - $legacyFormat = null; - if (str_ends_with($uriTemplate, '{._format}') || ($legacyFormat = str_ends_with($uriTemplate, '.{_format}'))) { + if (str_ends_with($uriTemplate, '{._format}')) { $uriTemplate = substr($uriTemplate, 0, -10); } - if ($legacyFormat && ($this->triggerLegacyFormatOnce[$operation->getClass()] ?? true)) { - $this->triggerLegacyFormatOnce[$operation->getClass()] = false; - trigger_deprecation('api-platform/core', '3.0', \sprintf('The special Symfony parameter ".{_format}" in your URI Template is deprecated, use an RFC6570 variable "{._format}" on the class "%s" instead. We will only use the RFC6570 compatible variable in 4.0.', $operation->getClass())); - } - if ($parameters = array_keys($uriVariables)) { foreach ($parameters as $parameterName) { $part = \sprintf('/{%s}', $parameterName); @@ -119,7 +111,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return \sprintf('%s%s', $uriTemplate, $legacyFormat ? '.{_format}' : '{._format}'); + return \sprintf('%s%s', $uriTemplate, '{._format}'); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation diff --git a/src/Metadata/ResourceClassResolverInterface.php b/src/Metadata/ResourceClassResolverInterface.php index 95e6426b3a2..303113e6312 100644 --- a/src/Metadata/ResourceClassResolverInterface.php +++ b/src/Metadata/ResourceClassResolverInterface.php @@ -15,40 +15,25 @@ use ApiPlatform\Metadata\Exception\InvalidArgumentException; -if (interface_exists(\ApiPlatform\Api\ResourceClassResolverInterface::class)) { - class_alias( - \ApiPlatform\Api\ResourceClassResolverInterface::class, - __NAMESPACE__.'\ResourceClassResolverInterface' - ); - - if (false) { // @phpstan-ignore-line - interface ResourceClassResolverInterface extends \ApiPlatform\Api\ResourceClassResolverInterface - { - } - } -} else { +/** + * Guesses which resource is associated with a given object. + * + * @author Kévin Dunglas + */ +interface ResourceClassResolverInterface +{ /** - * Guesses which resource is associated with a given object. + * Guesses the associated resource. + * + * @param string $resourceClass The expected resource class + * @param bool $strict If true, value must match the expected resource class * - * @author Kévin Dunglas + * @throws InvalidArgumentException */ - interface ResourceClassResolverInterface - { - /** - * Guesses the associated resource. - * - * @param string $resourceClass The expected resource class - * @param bool $strict If true, value must match the expected resource class - * - * @throws InvalidArgumentException - */ - public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string; + public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string; - /** - * Is the given class a resource class? - */ - public function isResourceClass(string $type): bool; - } - - class_alias(ResourceClassResolverInterface::class, \ApiPlatform\Api\ResourceClassResolverInterface::class); + /** + * Is the given class a resource class? + */ + public function isResourceClass(string $type): bool; } diff --git a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php index 0edb5f4ae41..5af03c71190 100644 --- a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php +++ b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php @@ -47,6 +47,9 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface 'property', ]; + // TODO: add serialize support for XML (policy is Laravel-only) + private const EXCLUDE = ['policy', 'serialize']; + /** * {@inheritdoc} */ @@ -68,6 +71,10 @@ public function __invoke(string $resourceClass, string $propertyName, array $par foreach ($parameters as $parameter) { $parameterName = $parameter->getName(); + if (\in_array($parameterName, self::EXCLUDE, true)) { + continue; + } + $value = \array_key_exists($parameterName, $fixtures) ? $fixtures[$parameterName] : null; if (method_exists($this, 'build'.ucfirst($parameterName))) { diff --git a/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php b/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php index 4a2acbbb069..03d9561fb35 100644 --- a/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php +++ b/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php @@ -68,6 +68,8 @@ final class XmlResourceAdapter implements ResourceAdapterInterface 'parameters', ]; + private const EXCLUDE = ['policy', 'middleware', 'rule']; + /** * {@inheritdoc} */ @@ -94,6 +96,10 @@ public function __invoke(string $resourceClass, array $parameters, array $fixtur $fixture['class'] = $resourceClass; foreach ($parameters as $parameter) { $parameterName = $parameter->getName(); + if (\in_array($parameterName, self::EXCLUDE, true)) { + continue; + } + $value = \array_key_exists($parameterName, $fixture) ? $fixture[$parameterName] : null; if ('compositeIdentifier' === $parameterName || 'provider' === $parameterName || 'processor' === $parameterName) { @@ -222,16 +228,6 @@ private function buildHydraContext(\SimpleXMLElement $resource, array $values): $this->buildValues($resource->addChild('hydraContext'), $values); } - /** - * TODO Remove in 4.0. - * - * @deprecated - */ - private function buildOpenapiContext(\SimpleXMLElement $resource, array $values): void - { - $this->buildValues($resource->addChild('openapiContext'), $values); - } - private function buildOpenapi(\SimpleXMLElement $resource, array $values): void { $node = $resource->openapi ?? $resource->addChild('openapi'); @@ -508,6 +504,10 @@ private function buildLinks(\SimpleXMLElement $resource, ?array $values = null): $childNode->addAttribute('href', $values[0]['href']); } + private function buildRules(\SimpleXMLElement $resource, ?array $values = null): void + { + } + private function buildHeaders(\SimpleXMLElement $resource, ?array $values = null): void { if (!$values) { diff --git a/src/Metadata/Tests/Extractor/Adapter/resources.xml b/src/Metadata/Tests/Extractor/Adapter/resources.xml index 45b012242b2..7f3ff308d27 100644 --- a/src/Metadata/Tests/Extractor/Adapter/resources.xml +++ b/src/Metadata/Tests/Extractor/Adapter/resources.xml @@ -1,3 +1,3 @@ -someirischemaanotheririschemaCommentapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetapplication/merge-patch+json+ldapplication/merge-patch+json+ld_foo\d+bazhttps
60120AuthorizationAccept-LanguageAcceptcomment:read_collectioncomment:writebazbazbazbarcomment.another_custom_filteruserIdLorem ipsum dolor sit ametDolor sit ametbarstringapplication/vnd.ms-excelapplication/merge-patch+jsonapplication/merge-patch+jsonpouet\d+barhttphttps60120AuthorizationAccept-Languagecomment:readcomment:writecomment:custombazbazbazbarcomment.custom_filterfoobarcustombazcustomquxcomment:read_collectioncomment:writebarcomment.another_custom_filteruserIdLorem ipsum dolor sit ametDolor sit ametbar/v1/v1Lorem ipsum dolor sit ametDolor sit amet/v1Lorem ipsum dolor sit ametDolor sit amet/v1Lorem ipsum dolor sit ametDolor sit ametLorem ipsum dolor sit ametDolor sit amet +someirischemaanotheririschemaCommentapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetapplication/merge-patch+json+ldapplication/merge-patch+json+ld_foo\d+bazhttps
60120AuthorizationAccept-LanguageAcceptcomment:read_collectioncomment:writebazbazbarcomment.another_custom_filteruserIdLorem ipsum dolor sit ametDolor sit ametbarstringapplication/vnd.ms-excelapplication/merge-patch+jsonapplication/merge-patch+jsonpouet\d+barhttphttps60120AuthorizationAccept-Languagecomment:readcomment:writecomment:custombazbazbarcomment.custom_filterfoobarcustombazcustomquxcomment:read_collectioncomment:writebarcomment.another_custom_filteruserIdLorem ipsum dolor sit ametDolor sit ametbar/v1/v1Lorem ipsum dolor sit ametDolor sit amet/v1Lorem ipsum dolor sit ametDolor sit amet/v1Lorem ipsum dolor sit ametDolor sit ametLorem ipsum dolor sit ametDolor sit amet diff --git a/src/Metadata/Tests/Extractor/Adapter/resources.yaml b/src/Metadata/Tests/Extractor/Adapter/resources.yaml index 2c9e21b1c5f..3316a0798cc 100644 --- a/src/Metadata/Tests/Extractor/Adapter/resources.yaml +++ b/src/Metadata/Tests/Extractor/Adapter/resources.yaml @@ -66,8 +66,6 @@ resources: hydraContext: foo: bar: baz - openapiContext: - bar: baz openapi: extensionProperties: bar: baz @@ -189,8 +187,6 @@ resources: hydraContext: foo: bar: baz - openapiContext: - bar: baz openapi: extensionProperties: bar: baz @@ -339,6 +335,9 @@ resources: elasticsearchOptions: index: foo_index type: foo_type + rules: null + policy: null + middleware: null parameters: null extraProperties: custom_property: 'Lorem ipsum dolor sit amet' diff --git a/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php index 9366b943b01..55ebdb5d919 100644 --- a/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php +++ b/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php @@ -78,9 +78,7 @@ final class PropertyMetadataCompatibilityTest extends TestCase 'property' => 'test', ]; - /** - * @dataProvider getExtractors - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getExtractors')] public function testValidMetadata(string $extractorClass, PropertyAdapterInterface $adapter): void { $reflClass = new \ReflectionClass(ApiProperty::class); diff --git a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php index 030f754e90c..d8dfc53f0f9 100644 --- a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php +++ b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php @@ -28,7 +28,6 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Metadata\Resource\Factory\ExtractorResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait; @@ -141,10 +140,6 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'hydraContext' => [ 'foo' => ['bar' => 'baz'], ], - // TODO Remove in 4.0 - 'openapiContext' => [ - 'bar' => 'baz', - ], 'openapi' => [ 'extensionProperties' => [ 'bar' => 'baz', @@ -364,10 +359,6 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'hydraContext' => [ 'foo' => ['bar' => 'baz'], ], - // TODO Remove in 4.0 - 'openapiContext' => [ - 'bar' => 'baz', - ], 'openapi' => [ 'extensionProperties' => [ 'bar' => 'baz', @@ -507,19 +498,16 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'schemes', 'cacheHeaders', 'hydraContext', - // TODO Remove in 4.0 - 'openapiContext', 'openapi', 'paginationViaCursor', 'stateOptions', 'links', + 'rules', 'headers', 'parameters', ]; - /** - * @dataProvider getExtractors - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getExtractors')] public function testValidMetadata(string $extractorClass, ResourceAdapterInterface $adapter): void { $reflClass = new \ReflectionClass(ApiResource::class); @@ -560,7 +548,7 @@ private function buildApiResources(): array if (null === $fixtures) { // Build default operations $operations = []; - foreach ([new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $operation) { + foreach ([new Get(), new GetCollection(), new Post(), new Patch(), new Delete()] as $operation) { [$name, $operation] = $this->getOperationWithDefaults($resource, $operation); $operations[$name] = $operation; } diff --git a/src/Metadata/Tests/Extractor/XmlExtractorTest.php b/src/Metadata/Tests/Extractor/XmlExtractorTest.php index b83f0ce1018..3f050a2963b 100644 --- a/src/Metadata/Tests/Extractor/XmlExtractorTest.php +++ b/src/Metadata/Tests/Extractor/XmlExtractorTest.php @@ -83,7 +83,6 @@ public function testValidXML(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -167,7 +166,6 @@ public function testValidXML(): void 'hydraContext' => [ 'foo' => ['bar' => 'baz'], ], - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => ['comment.custom_filter'], @@ -247,7 +245,6 @@ public function testValidXML(): void 'hydraContext' => [ 'foo' => ['bar' => 'baz'], ], - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => ['comment.custom_filter'], @@ -348,7 +345,6 @@ public function testValidXML(): void 'hydraContext' => [ 'foo' => ['bar' => 'baz'], ], - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => ['comment.custom_filter'], @@ -406,9 +402,7 @@ public function testValidXML(): void ], $extractor->getResources()); } - /** - * @dataProvider getInvalidPaths - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidPaths')] public function testInvalidXML(string $path, string $error): void { $this->expectException(InvalidArgumentException::class); diff --git a/src/Metadata/Tests/Extractor/YamlExtractorTest.php b/src/Metadata/Tests/Extractor/YamlExtractorTest.php index ea891fa654a..477d0bb5375 100644 --- a/src/Metadata/Tests/Extractor/YamlExtractorTest.php +++ b/src/Metadata/Tests/Extractor/YamlExtractorTest.php @@ -84,7 +84,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -157,7 +156,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -231,7 +229,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -297,7 +294,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -380,7 +376,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -470,7 +465,6 @@ public function testValidYaml(): void 'denormalizationContext' => null, 'collectDenormalizationErrors' => null, 'hydraContext' => null, - 'openapiContext' => null, 'openapi' => null, 'validationContext' => null, 'filters' => null, @@ -547,9 +541,7 @@ public function testInputAndOutputAreStrings(): void $this->assertSame(Program::class.'Output', $resources[Program::class][0]['operations'][0]['output']); } - /** - * @dataProvider getInvalidPaths - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getInvalidPaths')] public function testInvalidYaml(string $path, string $error): void { $this->expectException(InvalidArgumentException::class); diff --git a/src/Metadata/Tests/Property/Factory/CachedPropertyMetadataFactoryTest.php b/src/Metadata/Tests/Property/Factory/CachedPropertyMetadataFactoryTest.php index 56ef0037691..0abd9780e5b 100644 --- a/src/Metadata/Tests/Property/Factory/CachedPropertyMetadataFactoryTest.php +++ b/src/Metadata/Tests/Property/Factory/CachedPropertyMetadataFactoryTest.php @@ -92,6 +92,6 @@ public function testCreateWithGetCacheItemThrowsCacheException(): void private function generateCacheKey(string $resourceClass = Dummy::class, string $property = 'dummy', array $options = []): string { - return CachedPropertyMetadataFactory::CACHE_KEY_PREFIX.md5(serialize([$resourceClass, $property, $options])); + return CachedPropertyMetadataFactory::CACHE_KEY_PREFIX.hash('xxh3', serialize([$resourceClass, $property, $options])); } } diff --git a/src/Metadata/Tests/Property/Factory/CachedPropertyNameCollectionFactoryTest.php b/src/Metadata/Tests/Property/Factory/CachedPropertyNameCollectionFactoryTest.php index da9f146b47a..8ed75605627 100644 --- a/src/Metadata/Tests/Property/Factory/CachedPropertyNameCollectionFactoryTest.php +++ b/src/Metadata/Tests/Property/Factory/CachedPropertyNameCollectionFactoryTest.php @@ -92,6 +92,6 @@ public function testCreateWithGetCacheItemThrowsCacheException(): void private function generateCacheKey(string $resourceClass = Dummy::class, array $options = []): string { - return CachedPropertyNameCollectionFactory::CACHE_KEY_PREFIX.md5(serialize([$resourceClass, $options])); + return CachedPropertyNameCollectionFactory::CACHE_KEY_PREFIX.hash('xxh3', serialize([$resourceClass, $options])); } } diff --git a/src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php b/src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php index 1e282c256fc..b08293138fb 100644 --- a/src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php +++ b/src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php @@ -40,9 +40,7 @@ public static function groupsProvider(): array ]; } - /** - * @dataProvider groupsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('groupsProvider')] public function testCreate($readGroups, $writeGroups): void { $serializerClassMetadataFactoryProphecy = $this->prophesize(SerializerClassMetadataFactoryInterface::class); diff --git a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 124def411ec..2b38b63067c 100644 --- a/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -164,7 +164,6 @@ class: AttributeDefaultOperations::class, '_api_AttributeDefaultOperations_get' => (new Get())->withOperation($operation), '_api_AttributeDefaultOperations_get_collection' => (new GetCollection())->withOperation($operation), '_api_AttributeDefaultOperations_post' => (new Post())->withOperation($operation), - '_api_AttributeDefaultOperations_put' => (new Put())->withOperation($operation), '_api_AttributeDefaultOperations_patch' => (new Patch())->withOperation($operation), '_api_AttributeDefaultOperations_delete' => (new Delete())->withOperation($operation), ], @@ -208,7 +207,6 @@ class: AttributeDefaultOperations::class, '_api_AttributeDefaultOperations_get' => (new Get())->withOperation($operation), '_api_AttributeDefaultOperations_get_collection' => (new GetCollection())->withOperation($operation), '_api_AttributeDefaultOperations_post' => (new Post())->withOperation($operation), - '_api_AttributeDefaultOperations_put' => (new Put())->withOperation($operation), '_api_AttributeDefaultOperations_patch' => (new Patch())->withOperation($operation), '_api_AttributeDefaultOperations_delete' => (new Delete())->withOperation($operation), ], diff --git a/src/Metadata/Tests/Resource/Factory/FormatsResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/FormatsResourceMetadataCollectionFactoryTest.php index e6a679ed424..914bde9a53a 100644 --- a/src/Metadata/Tests/Resource/Factory/FormatsResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/FormatsResourceMetadataCollectionFactoryTest.php @@ -27,9 +27,7 @@ class FormatsResourceMetadataCollectionFactoryTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(ApiResource $previous, ApiResource $expected, array $formats = [], array $patchFormats = []): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); diff --git a/src/Metadata/Tests/Resource/Factory/InputOutputResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/InputOutputResourceMetadataCollectionFactoryTest.php index 1fd6eca64cd..d1ecffc1031 100644 --- a/src/Metadata/Tests/Resource/Factory/InputOutputResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/InputOutputResourceMetadataCollectionFactoryTest.php @@ -25,9 +25,7 @@ class InputOutputResourceMetadataCollectionFactoryTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider getAttributes - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getAttributes')] public function testInputOutputMetadata(mixed $input, ?array $expected): void { $resourceCollection = new ResourceMetadataCollection('Foo', [new ApiResource(input: $input)]); diff --git a/src/Metadata/Tests/Resource/Factory/LinkFactoryTest.php b/src/Metadata/Tests/Resource/Factory/LinkFactoryTest.php index 499db9e4104..62aa19001a1 100644 --- a/src/Metadata/Tests/Resource/Factory/LinkFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/LinkFactoryTest.php @@ -36,9 +36,7 @@ final class LinkFactoryTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider provideCreateLinksFromIdentifiersCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideCreateLinksFromIdentifiersCases')] public function testCreateLinksFromIdentifiers(array $propertyNames, bool $compositeIdentifier, array $expectedLinks, ?bool $idAsIdentifier = null): void { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); @@ -62,41 +60,39 @@ public static function provideCreateLinksFromIdentifiersCases(): \Generator { yield 'no identifiers no id' => [ ['slug'], - 'compositeIdentifier' => false, + false, [], ]; yield 'id detected as identifier' => [ ['id'], - 'compositeIdentifier' => false, + false, [(new Link())->withFromClass(AttributeResource::class)->withParameterName('id')->withIdentifiers(['id'])], ]; yield 'id forced as identifier' => [ ['id'], - 'compositeIdentifier' => false, + false, [(new Link())->withFromClass(AttributeResource::class)->withParameterName('id')->withIdentifiers(['id'])], true, ]; yield 'id forced as no identifier' => [ ['id'], - 'compositeIdentifier' => false, + false, [], false, ]; yield 'name identifier' => [ ['id', 'name'], - 'compositeIdentifier' => false, + false, [(new Link())->withFromClass(AttributeResource::class)->withParameterName('name')->withIdentifiers(['name'])], ]; yield 'composite identifier' => [ ['composite1', 'composite2'], - 'compositeIdentifier' => true, + true, [(new Link())->withFromClass(AttributeResource::class)->withParameterName('id')->withIdentifiers(['composite1', 'composite2'])->withCompositeIdentifier(true)], ]; } - /** - * @dataProvider provideCreateLinksFromAttributesCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideCreateLinksFromAttributesCases')] public function testCreateLinksFromAttributes(array $builtinTypes, array $expectedLinks): void { $propertyNameCollectionFactory = new PropertyInfoPropertyNameCollectionFactory(new PropertyInfoExtractor([new ReflectionExtractor()])); diff --git a/src/Metadata/Tests/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php index d2bc4a64450..9edf2b70006 100644 --- a/src/Metadata/Tests/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php @@ -75,7 +75,7 @@ class: AttributeResource::class, ]), ); - $linkResourceMetadataCollectionFactory = new LinkResourceMetadataCollectionFactory($linkFactory, $resourceCollectionMetadataFactoryProphecy->reveal()); + $linkResourceMetadataCollectionFactory = new LinkResourceMetadataCollectionFactory($linkFactory, $resourceCollectionMetadataFactoryProphecy->reveal(), true); $this->assertEquals( new ResourceMetadataCollection(AttributeResource::class, [ @@ -123,7 +123,7 @@ class: AttributeResource::class, ]), ); - $linkResourceMetadataCollectionFactory = new LinkResourceMetadataCollectionFactory($linkFactory, $resourceCollectionMetadataFactoryProphecy->reveal()); + $linkResourceMetadataCollectionFactory = new LinkResourceMetadataCollectionFactory($linkFactory, $resourceCollectionMetadataFactoryProphecy->reveal(), true); $this->assertEquals( new ResourceMetadataCollection(AttributeResource::class, [ diff --git a/src/Metadata/Tests/Resource/Factory/OperationNameResourceMetadataFactoryTest.php b/src/Metadata/Tests/Resource/Factory/OperationNameResourceMetadataFactoryTest.php index ef767d047c9..352053cd7a9 100644 --- a/src/Metadata/Tests/Resource/Factory/OperationNameResourceMetadataFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/OperationNameResourceMetadataFactoryTest.php @@ -26,9 +26,7 @@ class OperationNameResourceMetadataFactoryTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider operationProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] public function testGeneratesName(HttpOperation $operation, string $expectedOperationName): void { $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); diff --git a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTests.php b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTests.php index 99712574328..d845833e69c 100644 --- a/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTests.php +++ b/src/Metadata/Tests/Resource/Factory/ParameterResourceMetadataCollectionFactoryTests.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ParameterResourceMetadataCollectionFactory; @@ -27,6 +29,8 @@ class ParameterResourceMetadataCollectionFactoryTests extends TestCase { public function testParameterFactory(): void { + $nameCollection = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $propertyMetadata = $this->createStub(PropertyMetadataFactoryInterface::class); $filterLocator = $this->createStub(ContainerInterface::class); $filterLocator->method('has')->willReturn(true); $filterLocator->method('get')->willReturn(new class implements FilterInterface { @@ -48,7 +52,12 @@ public function getDescription(string $resourceClass): array ]; } }); - $parameter = new ParameterResourceMetadataCollectionFactory(new AttributesResourceMetadataCollectionFactory(), $filterLocator); + $parameter = new ParameterResourceMetadataCollectionFactory( + $nameCollection, + $propertyMetadata, + new AttributesResourceMetadataCollectionFactory(), + $filterLocator + ); $operation = $parameter->create(WithParameter::class)->getOperation('collection'); $this->assertInstanceOf(Parameters::class, $parameters = $operation->getParameters()); $hydraParameter = $parameters->get('hydra', QueryParameter::class); diff --git a/src/Metadata/Tests/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index 7f3f5cb48db..57a51109a6c 100644 --- a/src/Metadata/Tests/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -23,7 +23,6 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\LinkFactory; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory; @@ -31,18 +30,15 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\AttributeResource; use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\Metadata\Tests\Fixtures\ApiResourceNotLoaded\SymfonyFormatParameterLegacy; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; /** * @author Antoine Bluchet */ class UriTemplateResourceMetadataCollectionFactoryTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; public function testCreate(): void @@ -226,25 +222,4 @@ class: AttributeResource::class, $uriTemplateResourceMetadataCollectionFactory->create(AttributeResource::class) ); } - - /** - * @group legacy - */ - public function testCreateWithLegacyFormat(): void - { - $this->expectDeprecation('Since api-platform/core 3.0: The special Symfony parameter ".{_format}" in your URI Template is deprecated, use an RFC6570 variable "{._format}" on the class "ApiPlatform\Metadata\Tests\Fixtures\ApiResourceNotLoaded\SymfonyFormatParameterLegacy" instead. We will only use the RFC6570 compatible variable in 4.0.'); - - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Argument::cetera())->willReturn(new PropertyNameCollection()); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $linkFactory = new LinkFactory($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal()); - $pathSegmentNameGeneratorProphecy = $this->prophesize(PathSegmentNameGeneratorInterface::class); - $pathSegmentNameGeneratorProphecy->getSegmentName('SymfonyFormatParameterLegacy')->willReturn('attribute_resources'); - $resourceCollectionMetadataFactoryProphecy = new AttributesResourceMetadataCollectionFactory(); - - $linkFactory = new LinkFactory($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceClassResolverProphecy->reveal()); - $uriTemplateResourceMetadataCollectionFactory = new UriTemplateResourceMetadataCollectionFactory($linkFactory, $pathSegmentNameGeneratorProphecy->reveal(), $resourceCollectionMetadataFactoryProphecy); - $uriTemplateResourceMetadataCollectionFactory->create(SymfonyFormatParameterLegacy::class); - } } diff --git a/src/Metadata/Tests/Resource/OperationTest.php b/src/Metadata/Tests/Resource/OperationTest.php index 82781efedb2..c2d9e87a0e7 100644 --- a/src/Metadata/Tests/Resource/OperationTest.php +++ b/src/Metadata/Tests/Resource/OperationTest.php @@ -39,9 +39,7 @@ public function testWithResourceTrait(): void $this->assertSame($operation instanceof CollectionOperationInterface, true); } - /** - * @dataProvider operationProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] public function testOperationConstructor(Operation $operation): void { $this->assertInstanceOf(Operation::class, $operation); diff --git a/src/Metadata/Tests/Resource/StringableSecurityParameterTest.php b/src/Metadata/Tests/Resource/StringableSecurityParameterTest.php index a39c24a7327..680ac08b040 100644 --- a/src/Metadata/Tests/Resource/StringableSecurityParameterTest.php +++ b/src/Metadata/Tests/Resource/StringableSecurityParameterTest.php @@ -37,9 +37,7 @@ */ final class StringableSecurityParameterTest extends TestCase { - /** - * @dataProvider metadataProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('metadataProvider')] public function testOnMetadata(Metadata $metadata): void { $this->assertSame($metadata->getSecurity(), 'stringable_security'); diff --git a/tests/Api/UriVariableTransformer/DateTimeUriVariableTransformerTest.php b/src/Metadata/Tests/UriVariableTransformer/DateTimeUriVariableTransformerTest.php similarity index 83% rename from tests/Api/UriVariableTransformer/DateTimeUriVariableTransformerTest.php rename to src/Metadata/Tests/UriVariableTransformer/DateTimeUriVariableTransformerTest.php index 0bf748f56fb..c41337c5bb8 100644 --- a/tests/Api/UriVariableTransformer/DateTimeUriVariableTransformerTest.php +++ b/src/Metadata/Tests/UriVariableTransformer/DateTimeUriVariableTransformerTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Api\UriVariableTransformer; +namespace ApiPlatform\Metadata\Tests\UriVariableTransformer; -use ApiPlatform\Api\UriVariableTransformer\DateTimeUriVariableTransformer; -use ApiPlatform\Exception\InvalidUriVariableException; +use ApiPlatform\Metadata\Exception\InvalidUriVariableException; +use ApiPlatform\Metadata\UriVariableTransformer\DateTimeUriVariableTransformer; use PHPUnit\Framework\TestCase; class DateTimeUriVariableTransformerTest extends TestCase diff --git a/tests/Api/UriVariableTransformer/IntegerUriVariableTransformerTest.php b/src/Metadata/Tests/UriVariableTransformer/IntegerUriVariableTransformerTest.php similarity index 86% rename from tests/Api/UriVariableTransformer/IntegerUriVariableTransformerTest.php rename to src/Metadata/Tests/UriVariableTransformer/IntegerUriVariableTransformerTest.php index 7bfcf43bbd2..0a19f2e603b 100644 --- a/tests/Api/UriVariableTransformer/IntegerUriVariableTransformerTest.php +++ b/src/Metadata/Tests/UriVariableTransformer/IntegerUriVariableTransformerTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Api\UriVariableTransformer; +namespace ApiPlatform\Metadata\Tests\UriVariableTransformer; -use ApiPlatform\Api\UriVariableTransformer\IntegerUriVariableTransformer; +use ApiPlatform\Metadata\UriVariableTransformer\IntegerUriVariableTransformer; use PHPUnit\Framework\TestCase; class IntegerUriVariableTransformerTest extends TestCase diff --git a/tests/Api/CompositeIdentifierParserTest.php b/src/Metadata/Tests/Util/CompositeIdentifierParserTest.php similarity index 90% rename from tests/Api/CompositeIdentifierParserTest.php rename to src/Metadata/Tests/Util/CompositeIdentifierParserTest.php index 9737975330c..833c9d87551 100644 --- a/tests/Api/CompositeIdentifierParserTest.php +++ b/src/Metadata/Tests/Util/CompositeIdentifierParserTest.php @@ -11,16 +11,14 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Api; +namespace ApiPlatform\Metadata\Tests\Util; -use ApiPlatform\Api\CompositeIdentifierParser; +use ApiPlatform\Metadata\Util\CompositeIdentifierParser; use PHPUnit\Framework\TestCase; class CompositeIdentifierParserTest extends TestCase { - /** - * @dataProvider variousIdentifiers - */ + #[\PHPUnit\Framework\Attributes\DataProvider('variousIdentifiers')] public function testNormalizeCompositeCorrectly(array $identifiers): void { foreach ($identifiers as $string => $expected) { @@ -42,9 +40,7 @@ public static function variousIdentifiers(): array ]]]; } - /** - * @dataProvider compositeIdentifiers - */ + #[\PHPUnit\Framework\Attributes\DataProvider('compositeIdentifiers')] public function testStringify(array $identifiers): void { foreach ($identifiers as $string => $arr) { diff --git a/src/Metadata/Tests/Util/IriHelperTest.php b/src/Metadata/Tests/Util/IriHelperTest.php index a923233d9c8..5fcc2ac98d9 100644 --- a/src/Metadata/Tests/Util/IriHelperTest.php +++ b/src/Metadata/Tests/Util/IriHelperTest.php @@ -17,15 +17,12 @@ use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\IriHelper; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; /** * @author Kévin Dunglas */ class IriHelperTest extends TestCase { - use ExpectDeprecationTrait; - public function testHelpers(): void { $parsed = [ diff --git a/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php b/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php index a39d6046526..2fe0cf9d539 100644 --- a/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php +++ b/src/Metadata/Tests/Util/PropertyInfoToTypeInfoHelperTest.php @@ -28,34 +28,22 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeIdentifier; -/** - * @group legacy - */ class PropertyInfoToTypeInfoHelperTest extends TestCase { /** - * @dataProvider convertLegacyTypesToTypeDataProvider - * * @param list|null $legacyTypes */ + #[\PHPUnit\Framework\Attributes\DataProvider('convertLegacyTypesToTypeDataProvider')] public function testConvertLegacyTypesToType(?Type $type, ?array $legacyTypes): void { - if (!class_exists(Type::class)) { - $this->markTestSkipped('symfony/type-info requires PHP > 8.2'); - } - $this->assertEquals($type, PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($legacyTypes)); } /** * @return iterable|null}> */ - public function convertLegacyTypesToTypeDataProvider(): iterable + public static function convertLegacyTypesToTypeDataProvider(): iterable { - if (!class_exists(Type::class)) { - return; - } - yield [null, null]; yield [Type::null(), [new LegacyType('null')]]; // yield [Type::void(), [new LegacyType('void')]]; diff --git a/src/Metadata/UriVariablesConverterInterface.php b/src/Metadata/UriVariablesConverterInterface.php index 8320ba26746..22ba1e7214e 100644 --- a/src/Metadata/UriVariablesConverterInterface.php +++ b/src/Metadata/UriVariablesConverterInterface.php @@ -15,35 +15,22 @@ use ApiPlatform\Metadata\Exception\InvalidIdentifierException; -if (interface_exists(\ApiPlatform\Api\UriVariablesConverterInterface::class)) { - class_alias( - \ApiPlatform\Api\UriVariablesConverterInterface::class, - __NAMESPACE__.'\UriVariablesConverterInterface' - ); - - if (false) { // @phpstan-ignore-line - interface UriVariablesConverterInterface extends \ApiPlatform\Api\UriVariablesConverterInterface - { - } - } -} else { +/** + * Identifier converter. + * + * @author Antoine Bluchet + */ +interface UriVariablesConverterInterface +{ /** - * Identifier converter. + * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. + * + * @param array $data URI variables to convert to PHP values + * @param string $class The class to which the URI variables belong to + * + * @throws InvalidIdentifierException * - * @author Antoine Bluchet + * @return array Array indexed by identifiers properties with their values denormalized */ - interface UriVariablesConverterInterface - { - /** - * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. - * - * @param array $data URI variables to convert to PHP values - * @param string $class The class to which the URI variables belong to - * - * @throws InvalidIdentifierException - * - * @return array Array indexed by identifiers properties with their values denormalized - */ - public function convert(array $data, string $class, array $context = []): array; - } + public function convert(array $data, string $class, array $context = []): array; } diff --git a/src/Metadata/UrlGeneratorInterface.php b/src/Metadata/UrlGeneratorInterface.php index 82a412bbc41..5df27ef16e4 100644 --- a/src/Metadata/UrlGeneratorInterface.php +++ b/src/Metadata/UrlGeneratorInterface.php @@ -17,81 +17,68 @@ use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; use Symfony\Component\Routing\Exception\RouteNotFoundException; -if (interface_exists(\ApiPlatform\Api\UrlGeneratorInterface::class)) { - class_alias( - \ApiPlatform\Api\UrlGeneratorInterface::class, - __NAMESPACE__.'\UrlGeneratorInterface' - ); - - if (false) { // @phpstan-ignore-line - interface UrlGeneratorInterface extends \ApiPlatform\Api\UrlGeneratorInterface - { - } - } -} else { +/** + * UrlGeneratorInterface is the interface that all URL generator classes must implement. + * + * This interface has been imported and adapted from the Symfony project. + * + * The constants in this interface define the different types of resource references that + * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 + * We are using the term "URL" instead of "URI" as this is more common in web applications + * and we do not need to distinguish them as the difference is mostly semantical and + * less technical. Generating URIs, i.e. representation-independent resource identifiers, + * is also possible. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @copyright Fabien Potencier + */ +interface UrlGeneratorInterface +{ /** - * UrlGeneratorInterface is the interface that all URL generator classes must implement. - * - * This interface has been imported and adapted from the Symfony project. - * - * The constants in this interface define the different types of resource references that - * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 - * We are using the term "URL" instead of "URI" as this is more common in web applications - * and we do not need to distinguish them as the difference is mostly semantical and - * less technical. Generating URIs, i.e. representation-independent resource identifiers, - * is also possible. - * - * @author Fabien Potencier - * @author Tobias Schultze - * @copyright Fabien Potencier + * Generates an absolute URL, e.g. "http://example.com/dir/file". */ - interface UrlGeneratorInterface - { - /** - * Generates an absolute URL, e.g. "http://example.com/dir/file". - */ - public const ABS_URL = 0; + public const ABS_URL = 0; - /** - * Generates an absolute path, e.g. "/dir/file". - */ - public const ABS_PATH = 1; + /** + * Generates an absolute path, e.g. "/dir/file". + */ + public const ABS_PATH = 1; - /** - * Generates a relative path based on the current request path, e.g. "../parent-file". - * - * @see UrlGenerator::getRelativePath() - */ - public const REL_PATH = 2; + /** + * Generates a relative path based on the current request path, e.g. "../parent-file". + * + * @see UrlGenerator::getRelativePath() + */ + public const REL_PATH = 2; - /** - * Generates a network path, e.g. "//example.com/dir/file". - * Such reference reuses the current scheme but specifies the host. - */ - public const NET_PATH = 3; + /** + * Generates a network path, e.g. "//example.com/dir/file". + * Such reference reuses the current scheme but specifies the host. + */ + public const NET_PATH = 3; - /** - * Generates a URL or path for a specific route based on the given parameters. - * - * Parameters that reference placeholders in the route pattern will substitute them in the - * path or host. Extra params are added as query string to the URL. - * - * When the passed reference type cannot be generated for the route because it requires a different - * host or scheme than the current one, the method will return a more comprehensive reference - * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH - * but the route requires the https scheme whereas the current scheme is http, it will instead return an - * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches - * the route in any case. - * - * If there is no route with the given name, the generator must throw the RouteNotFoundException. - * - * The special parameter _fragment will be used as the document fragment suffixed to the final URL. - * - * @throws RouteNotFoundException If the named route doesn't exist - * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route - * @throws InvalidParameterException When a parameter value for a placeholder is not correct because - * it does not match the requirement - */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; - } + /** + * Generates a URL or path for a specific route based on the given parameters. + * + * Parameters that reference placeholders in the route pattern will substitute them in the + * path or host. Extra params are added as query string to the URL. + * + * When the passed reference type cannot be generated for the route because it requires a different + * host or scheme than the current one, the method will return a more comprehensive reference + * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH + * but the route requires the https scheme whereas the current scheme is http, it will instead return an + * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches + * the route in any case. + * + * If there is no route with the given name, the generator must throw the RouteNotFoundException. + * + * The special parameter _fragment will be used as the document fragment suffixed to the final URL. + * + * @throws RouteNotFoundException If the named route doesn't exist + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement + */ + public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; } diff --git a/src/Metadata/Util/CamelCaseToSnakeCaseNameConverter.php b/src/Metadata/Util/CamelCaseToSnakeCaseNameConverter.php index 2135d8a588f..fb042ff73d0 100644 --- a/src/Metadata/Util/CamelCaseToSnakeCaseNameConverter.php +++ b/src/Metadata/Util/CamelCaseToSnakeCaseNameConverter.php @@ -31,17 +31,14 @@ */ class CamelCaseToSnakeCaseNameConverter { - private $attributes; - private $lowerCamelCase; - /** * @param array|null $attributes The list of attributes to rename or null for all attributes * @param bool $lowerCamelCase Use lowerCamelCase style */ - public function __construct(?array $attributes = null, bool $lowerCamelCase = true) - { - $this->attributes = $attributes; - $this->lowerCamelCase = $lowerCamelCase; + public function __construct( + private readonly ?array $attributes = null, + private readonly bool $lowerCamelCase = true, + ) { } public function normalize(string $propertyName): string @@ -55,9 +52,11 @@ public function normalize(string $propertyName): string public function denormalize(string $propertyName): string { - $camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { - return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); - }, $propertyName); + $camelCasedName = preg_replace_callback( + '/(^|_|\.)+(.)/', + fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), + $propertyName + ); if ($this->lowerCamelCase) { $camelCasedName = lcfirst($camelCasedName); diff --git a/src/Metadata/Util/ResourceClassInfoTrait.php b/src/Metadata/Util/ResourceClassInfoTrait.php index 1c13550eed8..4c5f5126825 100644 --- a/src/Metadata/Util/ResourceClassInfoTrait.php +++ b/src/Metadata/Util/ResourceClassInfoTrait.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Metadata\Util; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; @@ -26,10 +25,7 @@ trait ResourceClassInfoTrait { use ClassInfoTrait; - /** - * @var LegacyResourceClassResolverInterface|ResourceClassResolverInterface|null - */ - private $resourceClassResolver; + private ?ResourceClassResolverInterface $resourceClassResolver = null; private ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null; /** diff --git a/src/Metadata/composer.json b/src/Metadata/composer.json index 0c96e8354ed..af7647e2020 100644 --- a/src/Metadata/composer.json +++ b/src/Metadata/composer.json @@ -27,22 +27,22 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "doctrine/inflector": "^1.0 || ^2.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/string": "^6.4 || ^7.0" + "symfony/property-info": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/type-info": "^7.1" }, "require-dev": { "api-platform/json-schema": "^3.4 || ^4.0", "api-platform/openapi": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpdoc-parser": "^1.16", - "sebastian/comparator": "<5.0", + "phpspec/prophecy-phpunit": "^2.2", + "phpstan/phpdoc-parser": "^1.13", + "phpunit/phpunit": "^11.2", "symfony/config": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/routing": "^6.4 || ^7.0", "symfony/var-dumper": "^6.4 || ^7.0", "symfony/web-link": "^6.4 || ^7.0", @@ -77,7 +77,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Metadata/phpunit.xml.dist b/src/Metadata/phpunit.xml.dist index f1d69794a2b..25405b0834b 100644 --- a/src/Metadata/phpunit.xml.dist +++ b/src/Metadata/phpunit.xml.dist @@ -1,35 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/OpenApi/Command/OpenApiCommand.php b/src/OpenApi/Command/OpenApiCommand.php index f2fd5ab8ac5..6f32b9a57ea 100644 --- a/src/OpenApi/Command/OpenApiCommand.php +++ b/src/OpenApi/Command/OpenApiCommand.php @@ -85,5 +85,3 @@ public static function getDefaultName(): string return 'api:openapi:export'; } } - -class_alias(OpenApiCommand::class, \ApiPlatform\Symfony\Bundle\Command\OpenApiCommand::class); diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 7bdf676f6a2..d83e83064b7 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -17,7 +17,6 @@ use ApiPlatform\Doctrine\Orm\State\Options as DoctrineOptions; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; -use ApiPlatform\JsonSchema\TypeFactoryInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Error; @@ -36,7 +35,6 @@ use ApiPlatform\OpenApi\Model; use ApiPlatform\OpenApi\Model\Components; use ApiPlatform\OpenApi\Model\Contact; -use ApiPlatform\OpenApi\Model\ExternalDocumentation; use ApiPlatform\OpenApi\Model\Info; use ApiPlatform\OpenApi\Model\License; use ApiPlatform\OpenApi\Model\Link; @@ -73,22 +71,15 @@ final class OpenApiFactory implements OpenApiFactoryInterface private readonly Options $openApiOptions; private readonly PaginationOptions $paginationOptions; private ?RouteCollection $routeCollection = null; - private ?TypeFactoryInterface $jsonSchemaTypeFactory = null; private ?ContainerInterface $filterLocator = null; - /** - * @deprecated use SchemaFactory::OPENAPI_DEFINITION_NAME this will be removed in API Platform 4 - */ - public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct( private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, - ?TypeFactoryInterface $jsonSchemaTypeFactory, - ContainerInterface $filterLocator, + ?ContainerInterface $filterLocator = null, private readonly array $formats = [], ?Options $openApiOptions = null, ?PaginationOptions $paginationOptions = null, @@ -97,11 +88,6 @@ public function __construct( $this->filterLocator = $filterLocator; $this->openApiOptions = $openApiOptions ?: new Options('API Platform'); $this->paginationOptions = $paginationOptions ?: new PaginationOptions(); - - if ($jsonSchemaTypeFactory) { - trigger_deprecation('api-platform/core', '3.4', \sprintf('Injecting the "%s" inside "%s" is deprecated and "%s" will be removed in 4.x.', TypeFactoryInterface::class, self::class, TypeFactoryInterface::class)); - $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory; - } } /** @@ -224,48 +210,6 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection [$requestMimeTypes, $responseMimeTypes] = $this->getMimeTypes($operation); - // TODO Remove in 4.0 - foreach (['operationId', 'tags', 'summary', 'description', 'security', 'servers'] as $key) { - if (null !== ($operation->getOpenapiContext()[$key] ?? null)) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $openapiOperation = $openapiOperation->{'with'.ucfirst($key)}($operation->getOpenapiContext()[$key]); - } - } - - // TODO Remove in 4.0 - if (null !== ($operation->getOpenapiContext()['externalDocs'] ?? null)) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $openapiOperation = $openapiOperation->withExternalDocs(new ExternalDocumentation($operation->getOpenapiContext()['externalDocs']['description'] ?? null, $operation->getOpenapiContext()['externalDocs']['url'])); - } - - // TODO Remove in 4.0 - if (null !== ($operation->getOpenapiContext()['callbacks'] ?? null)) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $openapiOperation = $openapiOperation->withCallbacks(new \ArrayObject($operation->getOpenapiContext()['callbacks'])); - } - - // TODO Remove in 4.0 - if (null !== ($operation->getOpenapiContext()['deprecated'] ?? null)) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $openapiOperation = $openapiOperation->withDeprecated((bool) $operation->getOpenapiContext()['deprecated']); - } - if ($path) { $pathItem = $paths->getPath($path) ?: new PathItem(); } elseif (!$pathItem) { @@ -284,20 +228,6 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions()); } - // TODO Remove in 4.0 - if ($operation->getOpenapiContext()['parameters'] ?? false) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $parameters = []; - foreach ($operation->getOpenapiContext()['parameters'] as $parameter) { - $parameters[] = new Parameter($parameter['name'], $parameter['in'], $parameter['description'] ?? '', $parameter['required'] ?? false, $parameter['deprecated'] ?? false, $parameter['allowEmptyValue'] ?? false, $parameter['schema'] ?? [], $parameter['style'] ?? null, $parameter['explode'] ?? false, $parameter['allowReserved '] ?? false, $parameter['example'] ?? null, isset($parameter['examples']) ? new \ArrayObject($parameter['examples']) : null, isset($parameter['content']) ? new \ArrayObject($parameter['content']) : null); - } - $openapiOperation = $openapiOperation->withParameters($parameters); - } - // Set up parameters $openapiParameters = $openapiOperation->getParameters(); foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariable) { @@ -338,7 +268,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection continue; } - if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator->has($f)) { + if (($f = $p->getFilter()) && \is_string($f) && $this->filterLocator && $this->filterLocator->has($f)) { $filter = $this->filterLocator->get($f); foreach ($filter->getDescription($entityClass) as $name => $description) { if ($prop = $p->getProperty()) { @@ -352,23 +282,42 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection } $in = $p instanceof HeaderParameterInterface ? 'header' : 'query'; - $parameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']); + $defaultParameter = new Parameter($key, $in, $p->getDescription() ?? "$resourceShortName $key", $p->getRequired() ?? false, false, false, $p->getSchema() ?? ['type' => 'string']); + + $linkParameter = $p->getOpenApi(); + if (null === $linkParameter) { + if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $defaultParameter)) { + $openapiParameters[$i] = $this->mergeParameter($defaultParameter, $operationParameter); + } else { + $openapiParameters[] = $defaultParameter; + } - if ($linkParameter = $p->getOpenApi()) { - $parameter = $this->mergeParameter($parameter, $linkParameter); + continue; + } + + if (\is_array($linkParameter)) { + foreach ($linkParameter as $lp) { + $parameter = $this->mergeParameter($defaultParameter, $lp); + if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) { + $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter); + continue; + } + + $openapiParameters[] = $parameter; + } + continue; } + $parameter = $this->mergeParameter($defaultParameter, $linkParameter); if ([$i, $operationParameter] = $this->hasParameter($openapiOperation, $parameter)) { $openapiParameters[$i] = $this->mergeParameter($parameter, $operationParameter); continue; } - $openapiParameters[] = $parameter; } $openapiOperation = $openapiOperation->withParameters($openapiParameters); - - $existingResponses = $openapiOperation?->getResponses() ?: []; + $existingResponses = $openapiOperation->getResponses() ?: []; $overrideResponses = $operation->getExtraProperties()[self::OVERRIDE_OPENAPI_RESPONSES] ?? $this->openApiOptions->getOverrideResponses(); if ($operation instanceof HttpOperation && null !== ($errors = $operation->getErrors())) { $openapiOperation = $this->addOperationErrors($openapiOperation, $errors, $responseMimeTypes, $resourceMetadataCollection, $schema, $schemas); @@ -419,27 +368,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection $openapiOperation = $openapiOperation->withResponse('default', new Response('Unexpected error')); } - if ($contextResponses = $operation->getOpenapiContext()['responses'] ?? false) { - // TODO Remove this "elseif" in 4.0 - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - foreach ($contextResponses as $statusCode => $contextResponse) { - $openapiOperation = $openapiOperation->withResponse($statusCode, new Response($contextResponse['description'] ?? '', isset($contextResponse['content']) ? new \ArrayObject($contextResponse['content']) : null, isset($contextResponse['headers']) ? new \ArrayObject($contextResponse['headers']) : null, isset($contextResponse['links']) ? new \ArrayObject($contextResponse['links']) : null)); - } - } - - if ($contextRequestBody = $operation->getOpenapiContext()['requestBody'] ?? false) { - // TODO Remove this "elseif" in 4.0 - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $openapiOperation = $openapiOperation->withRequestBody(new RequestBody($contextRequestBody['description'] ?? '', new \ArrayObject($contextRequestBody['content']), $contextRequestBody['required'] ?? false)); - } elseif ( + if ( \in_array($method, ['PATCH', 'PUT', 'POST'], true) && !(false === ($input = $operation->getInput()) || (\is_array($input) && null === $input['class'])) ) { @@ -461,32 +390,6 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection )); } - // TODO Remove in 4.0 - if (null !== $operation->getOpenapiContext() && \count($operation->getOpenapiContext())) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "openapiContext" option is deprecated, use "openapi" instead.' - ); - $allowedProperties = array_map(fn (\ReflectionProperty $reflProperty): string => $reflProperty->getName(), (new \ReflectionClass(Operation::class))->getProperties()); - foreach ($operation->getOpenapiContext() as $key => $value) { - $value = match ($key) { - 'externalDocs' => new ExternalDocumentation(description: $value['description'] ?? '', url: $value['url'] ?? ''), - 'requestBody' => new RequestBody(description: $value['description'] ?? '', content: isset($value['content']) ? new \ArrayObject($value['content'] ?? []) : null, required: $value['required'] ?? false), - 'callbacks' => new \ArrayObject($value ?? []), - 'parameters' => $openapiOperation->getParameters(), - default => $value, - }; - - if (\in_array($key, $allowedProperties, true)) { - $openapiOperation = $openapiOperation->{'with'.ucfirst($key)}($value); - continue; - } - - $openapiOperation = $openapiOperation->withExtensionProperty((string) $key, $value); - } - } - if ($openapiAttribute instanceof Webhook) { $webhooks[$openapiAttribute->getName()] = $pathItem->{'with'.ucfirst($method)}($openapiOperation); } else { @@ -711,7 +614,7 @@ private function getFilterParameter(string $name, array $description, string $sh $schema = $description['schema'] ?? []; if (isset($description['type']) && \in_array($description['type'], Type::$builtinTypes, true) && !isset($schema['type'])) { - $schema += $this->jsonSchemaTypeFactory ? $this->jsonSchemaTypeFactory->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)); + $schema += $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)); } if (!isset($schema['type'])) { @@ -738,11 +641,7 @@ private function getFilterParameter(string $name, array $description, string $sh } trigger_deprecation('api-platform/core', '4.0', \sprintf('Not using "%s" on the "openapi" field of the %s::getDescription() (%s) is deprecated.', Parameter::class, $filter, $shortName)); - if ($this->jsonSchemaTypeFactory) { - $schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false), 'openapi') : ['type' => 'string']); - } else { - $schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']); - } + $schema = $description['schema'] ?? (\in_array($description['type'], Type::$builtinTypes, true) ? $this->getType(new Type($description['type'], false, null, $description['is_collection'] ?? false)) : ['type' => 'string']); return new Parameter( $name, diff --git a/src/OpenApi/Serializer/ApiGatewayNormalizer.php b/src/OpenApi/Serializer/ApiGatewayNormalizer.php index 66e36e4bf1c..aaea891adca 100644 --- a/src/OpenApi/Serializer/ApiGatewayNormalizer.php +++ b/src/OpenApi/Serializer/ApiGatewayNormalizer.php @@ -14,9 +14,7 @@ namespace ApiPlatform\OpenApi\Serializer; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Removes features unsupported by Amazon API Gateway. @@ -27,7 +25,7 @@ * * @author Vincent Chalamon */ -final class ApiGatewayNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ApiGatewayNormalizer implements NormalizerInterface { public const API_GATEWAY = 'api_gateway'; private array $defaultContext = [ @@ -123,31 +121,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->documentationNormalizer, 'getSupportedTypes')) { - return ['*' => $this->documentationNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->documentationNormalizer->hasCacheableSupportsMethod()]; - } - return $this->documentationNormalizer->getSupportedTypes($format); } - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->documentationNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->documentationNormalizer->hasCacheableSupportsMethod(); - } - private function isLocalRef(string $ref): bool { return str_starts_with($ref, '#/'); diff --git a/src/OpenApi/Serializer/CacheableSupportsMethodInterface.php b/src/OpenApi/Serializer/CacheableSupportsMethodInterface.php deleted file mode 100644 index f131ecdaea1..00000000000 --- a/src/OpenApi/Serializer/CacheableSupportsMethodInterface.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\OpenApi\Serializer; - -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Serializer; - -if (method_exists(Serializer::class, 'getSupportedTypes')) { - /** - * Backward compatibility layer for getSupportedTypes(). - * - * @internal - * - * @author Kévin Dunglas - * - * @todo remove this interface when dropping support for Serializer < 6.3 - */ - interface CacheableSupportsMethodInterface - { - public function getSupportedTypes(?string $format): array; - } -} else { - /** - * Backward compatibility layer for NormalizerInterface::getSupportedTypes(). - * - * @internal - * - * @author Kévin Dunglas - * - * @todo remove this interface when dropping support for Serializer < 6.3 - */ - interface CacheableSupportsMethodInterface extends BaseCacheableSupportsMethodInterface - { - } -} diff --git a/src/OpenApi/Serializer/OpenApiNormalizer.php b/src/OpenApi/Serializer/OpenApiNormalizer.php index d061d566ae3..a5f1c2c4e19 100644 --- a/src/OpenApi/Serializer/OpenApiNormalizer.php +++ b/src/OpenApi/Serializer/OpenApiNormalizer.php @@ -18,12 +18,11 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Generates an OpenAPI v3 specification. */ -final class OpenApiNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class OpenApiNormalizer implements NormalizerInterface { public const FORMAT = 'json'; public const JSON_FORMAT = 'jsonopenapi'; @@ -82,20 +81,6 @@ public function getSupportedTypes($format): array return (self::FORMAT === $format || self::JSON_FORMAT === $format || self::YAML_FORMAT === $format) ? [OpenApi::class => true] : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - private function getPathsCallBack(): \Closure { return static function ($decoratedObject): array { diff --git a/src/OpenApi/Serializer/SerializerContextBuilder.php b/src/OpenApi/Serializer/SerializerContextBuilder.php index a7244a3a6fe..afb48243639 100644 --- a/src/OpenApi/Serializer/SerializerContextBuilder.php +++ b/src/OpenApi/Serializer/SerializerContextBuilder.php @@ -13,14 +13,13 @@ namespace ApiPlatform\OpenApi\Serializer; -use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; /** * @internal */ -final class SerializerContextBuilder implements SerializerContextBuilderInterface, LegacySerializerContextBuilderInterface +final class SerializerContextBuilder implements SerializerContextBuilderInterface { public function __construct(private readonly SerializerContextBuilderInterface $decorated) { diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index 1d3511df8e6..d95b74b1973 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -16,7 +16,6 @@ use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactory; -use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; @@ -66,13 +65,11 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; class OpenApiFactoryTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; private const OPERATION_FORMATS = [ @@ -500,7 +497,6 @@ public function testInvoke(): void $definitionNameFactory = new DefinitionNameFactory([]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceCollectionMetadataFactory, propertyNameCollectionFactory: $propertyNameCollectionFactory, propertyMetadataFactory: $propertyMetadataFactory, @@ -508,16 +504,12 @@ public function testInvoke(): void definitionNameFactory: $definitionNameFactory, ); - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactory); - $factory = new OpenApiFactory( $resourceNameCollectionFactoryProphecy->reveal(), $resourceCollectionMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $schemaFactory, - null, $filterLocatorProphecy->reveal(), [], new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [ @@ -883,7 +875,6 @@ public function testInvoke(): void 'type' => 'array', 'items' => ['type' => 'string'], ], 'deepObject', true), - new Parameter('order[name]', 'query', '', false, false, false, [ 'type' => 'string', 'enum' => ['asc', 'desc'], diff --git a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php index 418147a9d1d..defc4ad12f7 100644 --- a/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php +++ b/src/OpenApi/Tests/Serializer/OpenApiNormalizerTest.php @@ -15,7 +15,6 @@ use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\SchemaFactory; -use ApiPlatform\JsonSchema\TypeFactory; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; @@ -208,23 +207,18 @@ public function testNormalize(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactory, propertyNameCollectionFactory: $propertyNameCollectionFactory, propertyMetadataFactory: $propertyMetadataFactory, definitionNameFactory: $definitionNameFactory, ); - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactory); - $factory = new OpenApiFactory( $resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $schemaFactory, - null, $filterLocatorProphecy->reveal(), [], new Options('Test API', 'This is a test API.', '1.2.3', true, 'oauth2', 'authorizationCode', '/oauth/v2/token', '/oauth/v2/auth', '/oauth/v2/refresh', ['scope param'], [ diff --git a/src/OpenApi/composer.json b/src/OpenApi/composer.json index 4d7f5351960..3576b855da7 100644 --- a/src/OpenApi/composer.json +++ b/src/OpenApi/composer.json @@ -27,17 +27,17 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/json-schema": "^3.4 || ^4.0", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", "symfony/console": "^6.4 || ^7.0", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1" + "symfony/property-access": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2", "api-platform/doctrine-common": "^3.4 || ^4.0", "api-platform/doctrine-orm": "^3.4 || ^4.0", "api-platform/doctrine-odm": "^3.4 || ^4.0" @@ -66,7 +66,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/OpenApi/phpunit.xml.dist b/src/OpenApi/phpunit.xml.dist index eec6b863c42..338e9e11a5f 100644 --- a/src/OpenApi/phpunit.xml.dist +++ b/src/OpenApi/phpunit.xml.dist @@ -1,31 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Operation/DashPathSegmentNameGenerator.php b/src/Operation/DashPathSegmentNameGenerator.php deleted file mode 100644 index 88da093cf7f..00000000000 --- a/src/Operation/DashPathSegmentNameGenerator.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Operation; - -use ApiPlatform\Util\Inflector; - -/** - * Generate a path name with a dash separator according to a string and whether it's a collection or not. - * - * @deprecated replaced by ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator - * - * @author Antoine Bluchet - */ -final class DashPathSegmentNameGenerator implements PathSegmentNameGeneratorInterface -{ - public function __construct() - { - trigger_deprecation('api-platform', '3.1', \sprintf('%s is deprecated in favor of %s. This class will be removed in 4.0.', self::class, \ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator::class)); - } - - /** - * {@inheritdoc} - */ - public function getSegmentName(string $name, bool $collection = true): string - { - return $collection ? $this->dashize(Inflector::pluralize($name)) : $this->dashize($name); - } - - private function dashize(string $string): string - { - return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $string)); - } -} diff --git a/src/Operation/UnderscorePathSegmentNameGenerator.php b/src/Operation/UnderscorePathSegmentNameGenerator.php deleted file mode 100644 index c68bb7f8766..00000000000 --- a/src/Operation/UnderscorePathSegmentNameGenerator.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Operation; - -use ApiPlatform\Util\Inflector; - -/** - * Generate a path name with an underscore separator according to a string and whether it's a collection or not. - * - * @deprecated replaced by ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator - * - * @author Antoine Bluchet - */ -final class UnderscorePathSegmentNameGenerator implements PathSegmentNameGeneratorInterface -{ - public function __construct() - { - trigger_deprecation('api-platform', '3.1', \sprintf('%s is deprecated in favor of %s. This class will be removed in 4.0.', self::class, \ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator::class)); - } - - /** - * {@inheritdoc} - */ - public function getSegmentName(string $name, bool $collection = true): string - { - $name = Inflector::tableize($name); - - return $collection ? Inflector::pluralize($name) : $name; - } -} diff --git a/src/ParameterValidator/Exception/ValidationException.php b/src/ParameterValidator/Exception/ValidationException.php deleted file mode 100644 index b5559d5103f..00000000000 --- a/src/ParameterValidator/Exception/ValidationException.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Exception; - -use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; - -/** - * Filter validation exception. - * - * @author Julien DENIAU - */ -final class ValidationException extends \Exception implements ValidationExceptionInterface, ProblemExceptionInterface -{ - /** - * @param string[] $constraintViolationList - */ - public function __construct(private readonly array $constraintViolationList, string $message = '', int $code = 0, ?\Exception $previous = null) - { - parent::__construct($message ?: $this->__toString(), $code, $previous); - } - - public function getConstraintViolationList(): array - { - return $this->constraintViolationList; - } - - public function __toString(): string - { - return implode("\n", $this->constraintViolationList); - } - - public function getType(): string - { - return '/parameter_validation/'.$this->code; - } - - public function getTitle(): ?string - { - return $this->message ?: $this->__toString(); - } - - public function getStatus(): ?int - { - return 400; - } - - public function getDetail(): ?string - { - return $this->message ?: $this->__toString(); - } - - public function getInstance(): ?string - { - return null; - } -} diff --git a/src/ParameterValidator/Exception/ValidationExceptionInterface.php b/src/ParameterValidator/Exception/ValidationExceptionInterface.php deleted file mode 100644 index 863c5ff6854..00000000000 --- a/src/ParameterValidator/Exception/ValidationExceptionInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Exception; - -use ApiPlatform\Metadata\Exception\ExceptionInterface; - -/** - * This Exception is thrown when any parameter validation fails. - */ -interface ValidationExceptionInterface extends ExceptionInterface, \Stringable -{ -} diff --git a/src/ParameterValidator/FilterLocatorTrait.php b/src/ParameterValidator/FilterLocatorTrait.php deleted file mode 100644 index 5d4fbce277a..00000000000 --- a/src/ParameterValidator/FilterLocatorTrait.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator; - -use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\FilterInterface; -use Psr\Container\ContainerInterface; - -/** - * Manipulates filters with a backward compatibility between the new filter locator and the deprecated filter collection. - * - * @author Baptiste Meyer - * - * @deprecated - * - * @internal - */ -trait FilterLocatorTrait -{ - private ?ContainerInterface $filterLocator = null; - - /** - * Sets a filter locator with a backward compatibility. - */ - private function setFilterLocator(?ContainerInterface $filterLocator, bool $allowNull = false): void - { - if ($filterLocator instanceof ContainerInterface || (null === $filterLocator && $allowNull)) { - $this->filterLocator = $filterLocator; - } else { - throw new InvalidArgumentException(\sprintf('The "$filterLocator" argument is expected to be an implementation of the "%s" interface%s.', ContainerInterface::class, $allowNull ? ' or null' : '')); - } - } - - /** - * Gets a filter with a backward compatibility. - */ - private function getFilter(string $filterId): ?FilterInterface - { - if ($this->filterLocator && $this->filterLocator->has($filterId)) { - return $this->filterLocator->get($filterId); - } - - return null; - } -} diff --git a/src/ParameterValidator/ParameterValidator.php b/src/ParameterValidator/ParameterValidator.php deleted file mode 100644 index a915bd0381c..00000000000 --- a/src/ParameterValidator/ParameterValidator.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator; - -use ApiPlatform\ParameterValidator\Exception\ValidationException; -use ApiPlatform\ParameterValidator\Validator\ArrayItems; -use ApiPlatform\ParameterValidator\Validator\Bounds; -use ApiPlatform\ParameterValidator\Validator\Enum; -use ApiPlatform\ParameterValidator\Validator\Length; -use ApiPlatform\ParameterValidator\Validator\MultipleOf; -use ApiPlatform\ParameterValidator\Validator\Pattern; -use ApiPlatform\ParameterValidator\Validator\Required; -use Psr\Container\ContainerInterface; - -/** - * Validates parameters depending on filter description. - * - * @author Julien Deniau - */ -class ParameterValidator -{ - use FilterLocatorTrait; - - private array $validators; - - public function __construct(ContainerInterface $filterLocator) - { - $this->setFilterLocator($filterLocator); - - $this->validators = [ - new ArrayItems(), - new Bounds(), - new Enum(), - new Length(), - new MultipleOf(), - new Pattern(), - new Required(), - ]; - } - - public function validateFilters(string $resourceClass, array $resourceFilters, array $queryParameters): void - { - $errorList = []; - - foreach ($resourceFilters as $filterId) { - if (!$filter = $this->getFilter($filterId)) { - continue; - } - - foreach ($filter->getDescription($resourceClass) as $name => $data) { - foreach ($this->validators as $validator) { - if ($errors = $validator->validate($name, $data, $queryParameters)) { - $errorList[] = $errors; - } - } - } - } - - if ($errorList) { - throw new ValidationException(array_merge(...$errorList)); - } - } -} diff --git a/src/ParameterValidator/README.md b/src/ParameterValidator/README.md deleted file mode 100644 index ce572d2165e..00000000000 --- a/src/ParameterValidator/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# API Platform - Parameter Validator - -> [!CAUTION] -> -> This package has been **deprecated** and doesn't exist anymore in API Platform 4. -> Use [the Metadata package](https://github.com/api-platform/metadata) instead. - -The Parameter Validator component of the [API Platform](https://api-platform.com) framework. - -[Documentation](https://api-platform.com/docs/core/filters/) - -> [!CAUTION] -> -> This is a read-only sub split of `api-platform/core`, please -> [report issues](https://github.com/api-platform/core/issues) and -> [send Pull Requests](https://github.com/api-platform/core/pulls) -> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/ParameterValidator/Tests/ParameterValidatorTest.php b/src/ParameterValidator/Tests/ParameterValidatorTest.php deleted file mode 100644 index cc2223fb7df..00000000000 --- a/src/ParameterValidator/Tests/ParameterValidatorTest.php +++ /dev/null @@ -1,129 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests; - -use ApiPlatform\Metadata\FilterInterface; -use ApiPlatform\ParameterValidator\Exception\ValidationException; -use ApiPlatform\ParameterValidator\ParameterValidator; -use ApiPlatform\ParameterValidator\Tests\Fixtures\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Julien Deniau - */ -class ParameterValidatorTest extends TestCase -{ - use ProphecyTrait; - - private ParameterValidator $testedInstance; - private ObjectProphecy $filterLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->testedInstance = new ParameterValidator( - $this->filterLocatorProphecy->reveal() - ); - } - - /** - * unsafe method should not use filter validations. - * - * @doesNotPerformAssertions - */ - public function testOnKernelRequestWithUnsafeMethod(): void - { - $request = []; - - $this->testedInstance->validateFilters(Dummy::class, [], $request); - } - - /** - * If the tested filter is non-existent, then nothing should append. - * - * @doesNotPerformAssertions - */ - public function testOnKernelRequestWithWrongFilter(): void - { - $request = []; - - $this->filterLocatorProphecy->has('some_inexistent_filter')->willReturn(false); - $this->testedInstance->validateFilters(Dummy::class, ['some_inexistent_filter'], $request); - } - - /** - * if the required parameter is not set, throw an FilterValidationException. - */ - public function testOnKernelRequestWithRequiredFilterNotSet(): void - { - $request = []; - - $filterProphecy = $this->prophesize(FilterInterface::class); - $filterProphecy - ->getDescription(Dummy::class) - ->shouldBeCalled() - ->willReturn([ - 'required' => [ - 'required' => true, - ], - ]); - $this->filterLocatorProphecy - ->has('some_filter') - ->shouldBeCalled() - ->willReturn(true); - $this->filterLocatorProphecy - ->get('some_filter') - ->shouldBeCalled() - ->willReturn($filterProphecy->reveal()); - - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Query parameter "required" is required'); - $this->testedInstance->validateFilters(Dummy::class, ['some_filter'], $request); - } - - /** - * if the required parameter is set, no exception should be throwned. - */ - public function testOnKernelRequestWithRequiredFilter(): void - { - $request = ['required' => 'foo']; - - $this->filterLocatorProphecy - ->has('some_filter') - ->shouldBeCalled() - ->willReturn(true); - $filterProphecy = $this->prophesize(FilterInterface::class); - $filterProphecy - ->getDescription(Dummy::class) - ->shouldBeCalled() - ->willReturn([ - 'required' => [ - 'required' => true, - ], - ]); - $this->filterLocatorProphecy - ->get('some_filter') - ->shouldBeCalled() - ->willReturn($filterProphecy->reveal()); - - $this->testedInstance->validateFilters(Dummy::class, ['some_filter'], $request); - } -} diff --git a/src/ParameterValidator/Tests/Validator/ArrayItemsTest.php b/src/ParameterValidator/Tests/Validator/ArrayItemsTest.php deleted file mode 100644 index 6e922ba3396..00000000000 --- a/src/ParameterValidator/Tests/Validator/ArrayItemsTest.php +++ /dev/null @@ -1,371 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\ArrayItems; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class ArrayItemsTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $request = []; - $filter = new ArrayItems(); - - $this->assertEmpty( - $filter->validate('some_filter', [], $request) - ); - } - - public function testEmptyQueryParameter(): void - { - $request = ['some_filter' => '']; - $filter = new ArrayItems(); - - $this->assertEmpty( - $filter->validate('some_filter', [], $request) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingParameter(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'maxItems' => 3, - 'minItems' => 2, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'bar', 'foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain less than 3 values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $request = ['some_filter' => ['foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain more than 2 values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testNonMatchingParameterOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'maxItems' => 3, - 'minItems' => 2, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'bar', 'foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain less than 3 values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $request = ['some_filter' => ['foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain more than 2 values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testMatchingParameter(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'maxItems' => 3, - 'minItems' => 2, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $request = ['some_filter' => ['foo', 'bar', 'baz']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testMatchingParameterOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'maxItems' => 3, - 'minItems' => 2, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $request = ['some_filter' => ['foo', 'bar', 'baz']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingUniqueItems(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'uniqueItems' => true, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'bar', 'foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain unique values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testNonMatchingUniqueItemsOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'uniqueItems' => true, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'bar', 'foo']]; - $this->assertEquals( - ['Query parameter "some_filter" must contain unique values'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testMatchingUniqueItems(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'uniqueItems' => true, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'baz']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testMatchingUniqueItemsOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'uniqueItems' => true, - ], - ]; - - $request = ['some_filter' => ['foo', 'bar', 'baz']]; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testSeparators(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'maxItems' => 2, - 'uniqueItems' => true, - 'collectionFormat' => 'csv', - ], - ]; - - $request = ['some_filter' => 'foo,bar,bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['swagger']['collectionFormat'] = 'ssv'; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['swagger']['collectionFormat'] = 'ssv'; - $request = ['some_filter' => 'foo bar bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['swagger']['collectionFormat'] = 'tsv'; - $request = ['some_filter' => 'foo\tbar\tbar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['swagger']['collectionFormat'] = 'pipes'; - $request = ['some_filter' => 'foo|bar|bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testSeparatorsOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'maxItems' => 2, - 'uniqueItems' => true, - 'collectionFormat' => 'csv', - ], - ]; - - $request = ['some_filter' => 'foo,bar,bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['openapi']['collectionFormat'] = 'ssv'; - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['openapi']['collectionFormat'] = 'ssv'; - $request = ['some_filter' => 'foo bar bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['openapi']['collectionFormat'] = 'tsv'; - $request = ['some_filter' => 'foo\tbar\tbar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition['openapi']['collectionFormat'] = 'pipes'; - $request = ['some_filter' => 'foo|bar|bar']; - $this->assertEquals( - [ - 'Query parameter "some_filter" must contain less than 2 values', - 'Query parameter "some_filter" must contain unique values', - ], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testSeparatorsUnknownSeparator(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'swagger' => [ - 'maxItems' => 2, - 'uniqueItems' => true, - 'collectionFormat' => 'unknownFormat', - ], - ]; - $request = ['some_filter' => 'foo,bar,bar']; - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown collection format unknownFormat'); - - $filter->validate('some_filter', $filterDefinition, $request); - } - - public function testSeparatorsUnknownSeparatorOpenApi(): void - { - $filter = new ArrayItems(); - - $filterDefinition = [ - 'openapi' => [ - 'maxItems' => 2, - 'uniqueItems' => true, - 'collectionFormat' => 'unknownFormat', - ], - ]; - $request = ['some_filter' => 'foo,bar,bar']; - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unknown collection format unknownFormat'); - - $filter->validate('some_filter', $filterDefinition, $request); - } -} diff --git a/src/ParameterValidator/Tests/Validator/BoundsTest.php b/src/ParameterValidator/Tests/Validator/BoundsTest.php deleted file mode 100644 index 2706f8e9e23..00000000000 --- a/src/ParameterValidator/Tests/Validator/BoundsTest.php +++ /dev/null @@ -1,325 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\Bounds; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class BoundsTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $filter = new Bounds(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - } - - public function testEmptyQueryParameter(): void - { - $filter = new Bounds(); - - $this->assertEmpty( - $filter->validate('some_filter', [], ['some_filter' => '']) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingMinimum(): void - { - $request = ['some_filter' => '9']; - $filter = new Bounds(); - - $filterDefinition = [ - 'swagger' => [ - 'minimum' => 10, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'minimum' => 10, - 'exclusiveMinimum' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'minimum' => 9, - 'exclusiveMinimum' => true, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than 9'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testNonMatchingMinimumOpenApi(): void - { - $request = ['some_filter' => '9']; - $filter = new Bounds(); - - $filterDefinition = [ - 'openapi' => [ - 'minimum' => 10, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'minimum' => 10, - 'exclusiveMinimum' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'minimum' => 9, - 'exclusiveMinimum' => true, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be greater than 9'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testMatchingMinimum(): void - { - $request = ['some_filter' => '10']; - $filter = new Bounds(); - - $filterDefinition = [ - 'swagger' => [ - 'minimum' => 10, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'minimum' => 9, - 'exclusiveMinimum' => false, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testMatchingMinimumOpenApi(): void - { - $request = ['some_filter' => '10']; - $filter = new Bounds(); - - $filterDefinition = [ - 'openapi' => [ - 'minimum' => 10, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'minimum' => 9, - 'exclusiveMinimum' => false, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingMaximum(): void - { - $request = ['some_filter' => '11']; - $filter = new Bounds(); - - $filterDefinition = [ - 'swagger' => [ - 'maximum' => 10, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'maximum' => 10, - 'exclusiveMaximum' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'maximum' => 9, - 'exclusiveMaximum' => true, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than 9'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testNonMatchingMaximumOpenApi(): void - { - $request = ['some_filter' => '11']; - $filter = new Bounds(); - - $filterDefinition = [ - 'openapi' => [ - 'maximum' => 10, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'maximum' => 10, - 'exclusiveMaximum' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than or equal to 10'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'maximum' => 9, - 'exclusiveMaximum' => true, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be less than 9'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testMatchingMaximum(): void - { - $request = ['some_filter' => '10']; - $filter = new Bounds(); - - $filterDefinition = [ - 'swagger' => [ - 'maximum' => 10, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'swagger' => [ - 'maximum' => 10, - 'exclusiveMaximum' => false, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testMatchingMaximumOpenApi(): void - { - $request = ['some_filter' => '10']; - $filter = new Bounds(); - - $filterDefinition = [ - 'openapi' => [ - 'maximum' => 10, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - - $filterDefinition = [ - 'openapi' => [ - 'maximum' => 10, - 'exclusiveMaximum' => false, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } -} diff --git a/src/ParameterValidator/Tests/Validator/EnumTest.php b/src/ParameterValidator/Tests/Validator/EnumTest.php deleted file mode 100644 index 7f21ca17c0c..00000000000 --- a/src/ParameterValidator/Tests/Validator/EnumTest.php +++ /dev/null @@ -1,109 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\Enum; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class EnumTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $filter = new Enum(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - } - - public function testEmptyQueryParameter(): void - { - $filter = new Enum(); - - $this->assertEmpty( - $filter->validate('some_filter', [], ['some_filter' => '']) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingParameter(): void - { - $filter = new Enum(); - - $filterDefinition = [ - 'swagger' => [ - 'enum' => ['foo', 'bar'], - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be one of "foo, bar"'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'foobar']) - ); - } - - public function testNonMatchingParameterOpenApi(): void - { - $filter = new Enum(); - - $filterDefinition = [ - 'openapi' => [ - 'enum' => ['foo', 'bar'], - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must be one of "foo, bar"'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'foobar']) - ); - } - - /** - * @group legacy - */ - public function testMatchingParameter(): void - { - $filter = new Enum(); - - $filterDefinition = [ - 'swagger' => [ - 'enum' => ['foo', 'bar'], - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'foo']) - ); - } - - public function testMatchingParameterOpenApi(): void - { - $filter = new Enum(); - - $filterDefinition = [ - 'openapi' => [ - 'enum' => ['foo', 'bar'], - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'foo']) - ); - } -} diff --git a/src/ParameterValidator/Tests/Validator/LengthTest.php b/src/ParameterValidator/Tests/Validator/LengthTest.php deleted file mode 100644 index cb9a244a56c..00000000000 --- a/src/ParameterValidator/Tests/Validator/LengthTest.php +++ /dev/null @@ -1,249 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\Length; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class LengthTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $filter = new Length(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - } - - public function testEmptyQueryParameter(): void - { - $filter = new Length(); - - $this->assertEmpty( - $filter->validate('some_filter', [], ['some_filter' => '']) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingParameter(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'swagger' => [ - 'minLength' => 3, - 'maxLength' => 5, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be greater than or equal to 3'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'ab']) - ); - - $this->assertEquals( - ['Query parameter "some_filter" length must be lower than or equal to 5'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcdef']) - ); - } - - public function testNonMatchingParameterOpenApi(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'openapi' => [ - 'minLength' => 3, - 'maxLength' => 5, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be greater than or equal to 3'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'ab']) - ); - - $this->assertEquals( - ['Query parameter "some_filter" length must be lower than or equal to 5'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcdef']) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingParameterWithOnlyOneDefinition(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'swagger' => [ - 'minLength' => 3, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be greater than or equal to 3'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'ab']) - ); - - $filterDefinition = [ - 'swagger' => [ - 'maxLength' => 5, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be lower than or equal to 5'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcdef']) - ); - } - - public function testNonMatchingParameterWithOnlyOneDefinitionOpenApi(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'openapi' => [ - 'minLength' => 3, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be greater than or equal to 3'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'ab']) - ); - - $filterDefinition = [ - 'openapi' => [ - 'maxLength' => 5, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" length must be lower than or equal to 5'], - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcdef']) - ); - } - - /** - * @group legacy - */ - public function testMatchingParameter(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'swagger' => [ - 'minLength' => 3, - 'maxLength' => 5, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abc']) - ); - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcd']) - ); - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcde']) - ); - } - - public function testMatchingParameterOpenApi(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'openapi' => [ - 'minLength' => 3, - 'maxLength' => 5, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abc']) - ); - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcd']) - ); - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcde']) - ); - } - - /** - * @group legacy - */ - public function testMatchingParameterWithOneDefinition(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'swagger' => [ - 'minLength' => 3, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abc']) - ); - - $filterDefinition = [ - 'swagger' => [ - 'maxLength' => 5, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcde']) - ); - } - - public function testMatchingParameterWithOneDefinitionOpenApi(): void - { - $filter = new Length(); - - $filterDefinition = [ - 'openapi' => [ - 'minLength' => 3, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abc']) - ); - - $filterDefinition = [ - 'openapi' => [ - 'maxLength' => 5, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, ['some_filter' => 'abcde']) - ); - } -} diff --git a/src/ParameterValidator/Tests/Validator/MultipleOfTest.php b/src/ParameterValidator/Tests/Validator/MultipleOfTest.php deleted file mode 100644 index 58227b76725..00000000000 --- a/src/ParameterValidator/Tests/Validator/MultipleOfTest.php +++ /dev/null @@ -1,114 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\MultipleOf; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class MultipleOfTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $filter = new MultipleOf(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - } - - public function testEmptyQueryParameter(): void - { - $request = ['some_filter' => '']; - $filter = new MultipleOf(); - - $this->assertEmpty( - $filter->validate('some_filter', [], $request) - ); - } - - /** - * @group legacy - */ - public function testNonMatchingParameter(): void - { - $request = ['some_filter' => '8']; - $filter = new MultipleOf(); - - $filterDefinition = [ - 'swagger' => [ - 'multipleOf' => 3, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must multiple of 3'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testNonMatchingParameterOpenApi(): void - { - $request = ['some_filter' => '8']; - $filter = new MultipleOf(); - - $filterDefinition = [ - 'openapi' => [ - 'multipleOf' => 3, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must multiple of 3'], - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testMatchingParameter(): void - { - $request = ['some_filter' => '8']; - $filter = new MultipleOf(); - - $filterDefinition = [ - 'swagger' => [ - 'multipleOf' => 4, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } - - public function testMatchingParameterOpenApi(): void - { - $request = ['some_filter' => '8']; - $filter = new MultipleOf(); - - $filterDefinition = [ - 'openapi' => [ - 'multipleOf' => 4, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $filterDefinition, $request) - ); - } -} diff --git a/src/ParameterValidator/Tests/Validator/PatternTest.php b/src/ParameterValidator/Tests/Validator/PatternTest.php deleted file mode 100644 index bfa987a5240..00000000000 --- a/src/ParameterValidator/Tests/Validator/PatternTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\Pattern; -use PHPUnit\Framework\TestCase; - -/** - * @author Julien Deniau - */ -class PatternTest extends TestCase -{ - public function testNonDefinedFilter(): void - { - $filter = new Pattern(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - } - - /** - * @group legacy - */ - public function testFilterWithEmptyValue(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'swagger' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => '']) - ); - - $weirdParameter = new \stdClass(); - $weirdParameter->foo = 'non string value should not exists'; - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => $weirdParameter]) - ); - } - - public function testFilterWithEmptyValueOpenApi(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'openapi' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => '']) - ); - - $weirdParameter = new \stdClass(); - $weirdParameter->foo = 'non string value should not exists'; - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => $weirdParameter]) - ); - } - - /** - * @group legacy - */ - public function testFilterWithZeroAsParameter(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'swagger' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must match pattern /foo/'], - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => '0']) - ); - } - - public function testFilterWithZeroAsParameterOpenApi(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'openapi' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must match pattern /foo/'], - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => '0']) - ); - } - - /** - * @group legacy - */ - public function testFilterWithNonMatchingValue(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'swagger' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must match pattern /foo/'], - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => 'bar']) - ); - } - - public function testFilterWithNonMatchingValueOpenApi(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'openapi' => [ - 'pattern' => '/foo/', - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" must match pattern /foo/'], - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => 'bar']) - ); - } - - /** - * @group legacy - */ - public function testFilterWithNonchingValue(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'swagger' => [ - 'pattern' => '/foo \d+/', - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => 'this is a foo '.random_int(0, 10).' and it should match']) - ); - } - - public function testFilterWithNonchingValueOpenApi(): void - { - $filter = new Pattern(); - - $explicitFilterDefinition = [ - 'openapi' => [ - 'pattern' => '/foo \d+/', - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, ['some_filter' => 'this is a foo '.random_int(0, 10).' and it should match']) - ); - } -} diff --git a/src/ParameterValidator/Tests/Validator/RequiredTest.php b/src/ParameterValidator/Tests/Validator/RequiredTest.php deleted file mode 100644 index ce6419bca0f..00000000000 --- a/src/ParameterValidator/Tests/Validator/RequiredTest.php +++ /dev/null @@ -1,183 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Tests\Validator; - -use ApiPlatform\ParameterValidator\Validator\Required; -use PHPUnit\Framework\TestCase; - -/** - * Class RequiredTest. - * - * @author Julien Deniau - */ -class RequiredTest extends TestCase -{ - public function testNonRequiredFilter(): void - { - $request = []; - $filter = new Required(); - - $this->assertEmpty( - $filter->validate('some_filter', [], []) - ); - - $this->assertEmpty( - $filter->validate('some_filter', ['required' => false], $request) - ); - } - - public function testRequiredFilterNotInQuery(): void - { - $request = []; - $filter = new Required(); - - $this->assertEquals( - ['Query parameter "some_filter" is required'], - $filter->validate('some_filter', ['required' => true], $request) - ); - } - - public function testRequiredFilterIsPresent(): void - { - $request = ['some_filter' => 'some_value']; - $filter = new Required(); - - $this->assertEmpty( - $filter->validate('some_filter', ['required' => true], $request) - ); - } - - /** - * @group legacy - */ - public function testEmptyValueNotAllowed(): void - { - $request = ['some_filter' => '']; - $filter = new Required(); - - $explicitFilterDefinition = [ - 'required' => true, - 'swagger' => [ - 'allowEmptyValue' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" does not allow empty value'], - $filter->validate('some_filter', $explicitFilterDefinition, $request) - ); - - $implicitFilterDefinition = [ - 'required' => true, - ]; - - $this->assertEquals( - ['Query parameter "some_filter" does not allow empty value'], - $filter->validate('some_filter', $implicitFilterDefinition, $request) - ); - } - - public function testEmptyValueNotAllowedOpenApi(): void - { - $request = ['some_filter' => '']; - $filter = new Required(); - - $explicitFilterDefinition = [ - 'required' => true, - 'openapi' => [ - 'allowEmptyValue' => false, - ], - ]; - - $this->assertEquals( - ['Query parameter "some_filter" does not allow empty value'], - $filter->validate('some_filter', $explicitFilterDefinition, $request) - ); - - $implicitFilterDefinition = [ - 'required' => true, - ]; - - $this->assertEquals( - ['Query parameter "some_filter" does not allow empty value'], - $filter->validate('some_filter', $implicitFilterDefinition, $request) - ); - } - - /** - * @group legacy - */ - public function testEmptyValueAllowed(): void - { - $request = ['some_filter' => '']; - $filter = new Required(); - - $explicitFilterDefinition = [ - 'required' => true, - 'swagger' => [ - 'allowEmptyValue' => true, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, $request) - ); - } - - public function testEmptyValueAllowedOpenApi(): void - { - $request = ['some_filter' => '']; - $filter = new Required(); - - $explicitFilterDefinition = [ - 'required' => true, - 'openapi' => [ - 'allowEmptyValue' => true, - ], - ]; - - $this->assertEmpty( - $filter->validate('some_filter', $explicitFilterDefinition, $request) - ); - } - - public function testBracketNotation(): void - { - $filter = new Required(); - - $request = ['foo' => ['bar' => ['bar']]]; - - $requiredFilter = [ - 'required' => true, - ]; - - $this->assertEmpty( - $filter->validate('foo[bar]', $requiredFilter, $request) - ); - } - - public function testDotNotation(): void - { - $request = ['foo.bar' => 'bar']; - $filter = new Required(); - - $requiredFilter = [ - 'required' => true, - ]; - - $this->assertEmpty( - $filter->validate('foo.bar', $requiredFilter, $request) - ); - } -} diff --git a/src/ParameterValidator/Validator/ArrayItems.php b/src/ParameterValidator/Validator/ArrayItems.php deleted file mode 100644 index 28d99ff413c..00000000000 --- a/src/ParameterValidator/Validator/ArrayItems.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class ArrayItems implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - if (!\array_key_exists($name, $queryParameters)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxItems = $filterDescription['openapi']['maxItems'] ?? $filterDescription['swagger']['maxItems'] ?? null; - $minItems = $filterDescription['openapi']['minItems'] ?? $filterDescription['swagger']['minItems'] ?? null; - $uniqueItems = $filterDescription['openapi']['uniqueItems'] ?? $filterDescription['swagger']['uniqueItems'] ?? false; - - $errorList = []; - - $value = $this->getValue($name, $filterDescription, $queryParameters); - $nbItems = \count($value); - - if (null !== $maxItems && $nbItems > $maxItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain less than %d values', $name, $maxItems); - } - - if (null !== $minItems && $nbItems < $minItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain more than %d values', $name, $minItems); - } - - if (true === $uniqueItems && $nbItems > \count(array_unique($value))) { - $errorList[] = \sprintf('Query parameter "%s" must contain unique values', $name); - } - - return $errorList; - } - - private function getValue(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - - if (empty($value) && '0' !== $value) { - return []; - } - - if (\is_array($value)) { - return $value; - } - - $collectionFormat = $filterDescription['openapi']['collectionFormat'] ?? $filterDescription['swagger']['collectionFormat'] ?? 'csv'; - - return explode(self::getSeparator($collectionFormat), (string) $value) ?: []; // @phpstan-ignore-line - } - - /** - * @return non-empty-string - */ - private static function getSeparator(string $collectionFormat): string - { - return match ($collectionFormat) { - 'csv' => ',', - 'ssv' => ' ', - 'tsv' => '\t', - 'pipes' => '|', - default => throw new \InvalidArgumentException(\sprintf('Unknown collection format %s', $collectionFormat)), - }; - } -} diff --git a/src/ParameterValidator/Validator/Bounds.php b/src/ParameterValidator/Validator/Bounds.php deleted file mode 100644 index a5ca73fda41..00000000000 --- a/src/ParameterValidator/Validator/Bounds.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class Bounds implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maximum = $filterDescription['openapi']['maximum'] ?? $filterDescription['swagger']['maximum'] ?? null; - $minimum = $filterDescription['openapi']['minimum'] ?? $filterDescription['swagger']['minimum'] ?? null; - - $errorList = []; - - if (null !== $maximum) { - if (($filterDescription['openapi']['exclusiveMaximum'] ?? $filterDescription['swagger']['exclusiveMaximum'] ?? false) && $value >= $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than %s', $name, $maximum); - } elseif ($value > $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than or equal to %s', $name, $maximum); - } - } - - if (null !== $minimum) { - if (($filterDescription['openapi']['exclusiveMinimum'] ?? $filterDescription['swagger']['exclusiveMinimum'] ?? false) && $value <= $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than %s', $name, $minimum); - } elseif ($value < $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than or equal to %s', $name, $minimum); - } - } - - return $errorList; - } -} diff --git a/src/ParameterValidator/Validator/CheckFilterDeprecationsTrait.php b/src/ParameterValidator/Validator/CheckFilterDeprecationsTrait.php deleted file mode 100644 index 3658a699266..00000000000 --- a/src/ParameterValidator/Validator/CheckFilterDeprecationsTrait.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @internal - */ -trait CheckFilterDeprecationsTrait -{ - protected function checkFilterDeprecations(array $filterDescription): void - { - if (\array_key_exists('swagger', $filterDescription)) { - trigger_deprecation( - 'api-platform/core', - '3.0', - 'Using the "swagger" key in filters description is deprecated, use "openapi" instead.' - ); - } - } -} diff --git a/src/ParameterValidator/Validator/Enum.php b/src/ParameterValidator/Validator/Enum.php deleted file mode 100644 index b22b12faccd..00000000000 --- a/src/ParameterValidator/Validator/Enum.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class Enum implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $enum = $filterDescription['openapi']['enum'] ?? $filterDescription['swagger']['enum'] ?? null; - - if (null !== $enum && !\in_array($value, $enum, true)) { - return [ - \sprintf('Query parameter "%s" must be one of "%s"', $name, implode(', ', $enum)), - ]; - } - - return []; - } -} diff --git a/src/ParameterValidator/Validator/Length.php b/src/ParameterValidator/Validator/Length.php deleted file mode 100644 index 19e4aef3aee..00000000000 --- a/src/ParameterValidator/Validator/Length.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class Length implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxLength = $filterDescription['openapi']['maxLength'] ?? $filterDescription['swagger']['maxLength'] ?? null; - $minLength = $filterDescription['openapi']['minLength'] ?? $filterDescription['swagger']['minLength'] ?? null; - - $errorList = []; - - if (null !== $maxLength && mb_strlen($value) > $maxLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be lower than or equal to %s', $name, $maxLength); - } - - if (null !== $minLength && mb_strlen($value) < $minLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be greater than or equal to %s', $name, $minLength); - } - - return $errorList; - } -} diff --git a/src/ParameterValidator/Validator/MultipleOf.php b/src/ParameterValidator/Validator/MultipleOf.php deleted file mode 100644 index a363b471584..00000000000 --- a/src/ParameterValidator/Validator/MultipleOf.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class MultipleOf implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $multipleOf = $filterDescription['openapi']['multipleOf'] ?? $filterDescription['swagger']['multipleOf'] ?? null; - - if (null !== $multipleOf && 0 !== ($value % $multipleOf)) { - return [ - \sprintf('Query parameter "%s" must multiple of %s', $name, $multipleOf), - ]; - } - - return []; - } -} diff --git a/src/ParameterValidator/Validator/Pattern.php b/src/ParameterValidator/Validator/Pattern.php deleted file mode 100644 index ca29f05b77b..00000000000 --- a/src/ParameterValidator/Validator/Pattern.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -final class Pattern implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $pattern = $filterDescription['openapi']['pattern'] ?? $filterDescription['swagger']['pattern'] ?? null; - - if (null !== $pattern && !preg_match($pattern, $value)) { - return [ - \sprintf('Query parameter "%s" must match pattern %s', $name, $pattern), - ]; - } - - return []; - } -} diff --git a/src/ParameterValidator/Validator/Required.php b/src/ParameterValidator/Validator/Required.php deleted file mode 100644 index 41b6d2fdbaa..00000000000 --- a/src/ParameterValidator/Validator/Required.php +++ /dev/null @@ -1,109 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -use ApiPlatform\State\Util\RequestParser; - -/** - * @deprecated use Parameter constraint instead - */ -final class Required implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - // filter is not required, the `checkRequired` method can not break - if (!($filterDescription['required'] ?? false)) { - return []; - } - - // if query param is not given, then break - if (!$this->requestHasQueryParameter($queryParameters, $name)) { - return [ - \sprintf('Query parameter "%s" is required', $name), - ]; - } - - $this->checkFilterDeprecations($filterDescription); - - // if query param is empty and the configuration does not allow it - if (!($filterDescription['openapi']['allowEmptyValue'] ?? $filterDescription['swagger']['allowEmptyValue'] ?? false) && empty($this->requestGetQueryParameter($queryParameters, $name))) { - return [ - \sprintf('Query parameter "%s" does not allow empty value', $name), - ]; - } - - return []; - } - - /** - * Test if request has required parameter. - */ - private function requestHasQueryParameter(array $queryParameters, string $name): bool - { - $matches = RequestParser::parseRequestParams($name); - if (!$matches) { - return false; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return false; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - return \is_array($queryParameter) && isset($queryParameter[$keyName]); - } - - return \array_key_exists((string) $rootName, $queryParameters); - } - - /** - * Test if required filter is valid. It validates array notation too like "required[bar]". - */ - private function requestGetQueryParameter(array $queryParameters, string $name) - { - $matches = RequestParser::parseRequestParams($name); - if (empty($matches)) { - return null; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return null; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - if (\is_array($queryParameter) && isset($queryParameter[$keyName])) { - return $queryParameter[$keyName]; - } - - return null; - } - - return $queryParameters[(string) $rootName]; - } -} diff --git a/src/ParameterValidator/Validator/ValidatorInterface.php b/src/ParameterValidator/Validator/ValidatorInterface.php deleted file mode 100644 index 846c9ae4b01..00000000000 --- a/src/ParameterValidator/Validator/ValidatorInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\ParameterValidator\Validator; - -/** - * @deprecated use Parameter constraint instead - */ -interface ValidatorInterface -{ - /** - * @param string $name the parameter name to validate - * @param array $filterDescription the filter descriptions as returned by `\ApiPlatform\Metadata\FilterInterface::getDescription()` - * @param array $queryParameters the list of query parameter - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array; -} diff --git a/src/Problem/Serializer/ConstraintViolationListNormalizer.php b/src/Problem/Serializer/ConstraintViolationListNormalizer.php index a23f4255e7e..2fbcee0a671 100644 --- a/src/Problem/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Problem/Serializer/ConstraintViolationListNormalizer.php @@ -48,16 +48,6 @@ public function normalize(mixed $object, ?string $format = null, array $context { [$messages, $violations] = $this->getMessagesAndViolations($object); - // TODO: in api platform 4 this will be the default, as right now we serialize a ValidationException instead of a ConstraintViolationList - if ($context['rfc_7807_compliant_errors'] ?? false) { - return $violations; - } - - return [ - 'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE], - 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'detail' => $messages ? implode("\n", $messages) : (string) $object, - 'violations' => $violations, - ]; + return $violations; } } diff --git a/src/Problem/Serializer/ErrorNormalizer.php b/src/Problem/Serializer/ErrorNormalizer.php deleted file mode 100644 index f56373f2389..00000000000 --- a/src/Problem/Serializer/ErrorNormalizer.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Problem\Serializer; - -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\State\ApiResource\Error; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; - -/** - * Normalizes errors according to the API Problem spec (RFC 7807). - * - * @see https://tools.ietf.org/html/rfc7807 - * @deprecated - * - * @author Kévin Dunglas - */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ErrorNormalizerTrait; - public const FORMAT = 'jsonproblem'; - public const TYPE = 'type'; - public const TITLE = 'title'; - private array $defaultContext = [ - self::TYPE => 'https://tools.ietf.org/html/rfc2616#section-10', - self::TITLE => 'An error occurred', - ]; - - public function __construct(private readonly bool $debug = false, array $defaultContext = []) - { - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - } - - /** - * {@inheritdoc} - */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array - { - $data = [ - 'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE], - 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'detail' => $this->getErrorMessage($object, $context, $this->debug), - ]; - - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { - if ($context['api_error_resource'] ?? false) { - return false; - } - - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); - } - - public function getSupportedTypes($format): array - { - if (self::FORMAT === $format) { - return [ - \Exception::class => true, - Error::class => false, - FlattenException::class => true, - ]; - } - - return []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } -} diff --git a/src/Problem/Serializer/ErrorNormalizerTrait.php b/src/Problem/Serializer/ErrorNormalizerTrait.php deleted file mode 100644 index c8a1694dc24..00000000000 --- a/src/Problem/Serializer/ErrorNormalizerTrait.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Problem\Serializer; - -use ApiPlatform\Exception\ErrorCodeSerializableInterface; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Response; - -trait ErrorNormalizerTrait -{ - private function getErrorMessage($object, array $context, bool $debug = false): string - { - $message = $object->getMessage(); - - if ($debug) { - return $message; - } - - if ($object instanceof FlattenException) { - $statusCode = $context['statusCode'] ?? $object->getStatusCode(); - if ($statusCode >= 500 && $statusCode < 600) { - $message = Response::$statusTexts[$statusCode] ?? Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR]; - } - } - - return $message; - } - - private function getErrorCode(object $object): ?string - { - if ($object instanceof FlattenException) { - $exceptionClass = $object->getClass(); - } else { - $exceptionClass = $object::class; - } - - if (is_a($exceptionClass, ErrorCodeSerializableInterface::class, true)) { - return $exceptionClass::getErrorCode(); - } - - return null; - } -} diff --git a/src/RamseyUuid/composer.json b/src/RamseyUuid/composer.json index c611997bd4d..5d31ceb4fdf 100644 --- a/src/RamseyUuid/composer.json +++ b/src/RamseyUuid/composer.json @@ -22,16 +22,15 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", - "symfony/serializer": "^6.4 || ^7.1" + "symfony/serializer": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4", - "ramsey/uuid": "^3.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4", - "sebastian/comparator": "<5.0" + "phpspec/prophecy-phpunit": "^2.2", + "ramsey/uuid": "^4.0", + "ramsey/uuid-doctrine": "^2.0", + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": { @@ -54,7 +53,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/RamseyUuid/phpunit.xml.dist b/src/RamseyUuid/phpunit.xml.dist index d6eb5a25764..579531df2df 100644 --- a/src/RamseyUuid/phpunit.xml.dist +++ b/src/RamseyUuid/phpunit.xml.dist @@ -1,31 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Serializer/AbstractCollectionNormalizer.php b/src/Serializer/AbstractCollectionNormalizer.php index 7275012fa40..bf983c3dc55 100644 --- a/src/Serializer/AbstractCollectionNormalizer.php +++ b/src/Serializer/AbstractCollectionNormalizer.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; @@ -29,7 +28,7 @@ * * @author Baptiste Meyer */ -abstract class AbstractCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface +abstract class AbstractCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface { use ContextTrait { initContext as protected; @@ -42,7 +41,7 @@ abstract class AbstractCollectionNormalizer implements NormalizerInterface, Norm */ public const FORMAT = 'to-override'; - public function __construct(protected ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, protected string $pageParameterName, protected ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null) + public function __construct(protected ResourceClassResolverInterface $resourceClassResolver, protected string $pageParameterName, protected ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null) { } @@ -54,20 +53,6 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return static::FORMAT === $format && is_iterable($data); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - public function getSupportedTypes(?string $format): array { /* diff --git a/src/Serializer/AbstractConstraintViolationListNormalizer.php b/src/Serializer/AbstractConstraintViolationListNormalizer.php index 9e182fa1a0a..35907174547 100644 --- a/src/Serializer/AbstractConstraintViolationListNormalizer.php +++ b/src/Serializer/AbstractConstraintViolationListNormalizer.php @@ -17,7 +17,6 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -28,7 +27,7 @@ * * @internal */ -abstract class AbstractConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +abstract class AbstractConstraintViolationListNormalizer implements NormalizerInterface { public const FORMAT = null; // Must be overridden @@ -44,7 +43,7 @@ public function __construct(?array $serializePayloadFields = null, private reado */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - if (!isset($context['rfc_7807_compliant_errors']) && !($context['api_error_resource'] ?? false)) { + if (!($context['api_error_resource'] ?? false)) { return false; } @@ -56,20 +55,6 @@ public function getSupportedTypes($format): array return $format === static::FORMAT ? [ConstraintViolationListInterface::class => true] : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - protected function getMessagesAndViolations(ConstraintViolationListInterface $constraintViolationList): array { $violations = $messages = []; diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 09c16ce548e..f33f1e9f5f6 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; @@ -64,7 +62,7 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected array $localFactoryOptionsCache = []; protected ?ResourceAccessCheckerInterface $resourceAccessChecker; - public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected LegacyIriConverterInterface|IriConverterInterface $iriConverter, protected LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, protected PropertyMetadataFactoryInterface $propertyMetadataFactory, protected IriConverterInterface $iriConverter, protected ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { if (!isset($defaultContext['circular_reference_handler'])) { $defaultContext['circular_reference_handler'] = fn ($object): ?string => $this->iriConverter->getIriFromResource($object); @@ -100,23 +98,6 @@ public function getSupportedTypes(?string $format): array ]; } - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - /** * {@inheritdoc} * @@ -475,7 +456,7 @@ protected function canAccessAttribute(?object $object, string $attribute, array $options = $this->getFactoryOptions($context); $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options); - $security = $propertyMetadata->getSecurity(); + $security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy(); if (null !== $this->resourceAccessChecker && $security) { return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [ 'object' => $object, @@ -740,7 +721,6 @@ protected function getAttributeValue(object $object, string $attribute, ?string ) { $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']); - if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) { $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation( operationName: $uriTemplate, diff --git a/src/Serializer/CacheableSupportsMethodInterface.php b/src/Serializer/CacheableSupportsMethodInterface.php deleted file mode 100644 index 6fb8e0c6214..00000000000 --- a/src/Serializer/CacheableSupportsMethodInterface.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Serializer; - -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Serializer; - -if (method_exists(Serializer::class, 'getSupportedTypes')) { - /** - * Backward compatibility layer for getSupportedTypes(). - * - * @internal - * - * @author Kévin Dunglas - * - * @todo remove this interface when dropping support for Serializer < 6.3 - */ - interface CacheableSupportsMethodInterface - { - public function getSupportedTypes(?string $format): array; - } -} else { - /** - * Backward compatibility layer for NormalizerInterface::getSupportedTypes(). - * - * @internal - * - * @author Kévin Dunglas - * - * @todo remove this interface when dropping support for Serializer < 6.3 - */ - interface CacheableSupportsMethodInterface extends BaseCacheableSupportsMethodInterface - { - public function getSupportedTypes(?string $format): array; - } -} diff --git a/src/Serializer/Filter/PropertyFilter.php b/src/Serializer/Filter/PropertyFilter.php index 5d1c19235a2..188dba648e2 100644 --- a/src/Serializer/Filter/PropertyFilter.php +++ b/src/Serializer/Filter/PropertyFilter.php @@ -13,6 +13,11 @@ namespace ApiPlatform\Serializer\Filter; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter as MetadataParameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -113,7 +118,7 @@ * * @author Baptiste Meyer */ -final class PropertyFilter implements FilterInterface +final class PropertyFilter implements FilterInterface, OpenApiParameterFilterInterface, JsonSchemaFilterInterface { private ?array $whitelist; @@ -127,25 +132,40 @@ public function __construct(private readonly string $parameterName = 'properties */ public function apply(Request $request, bool $normalization, array $attributes, array &$context): void { + // TODO: ideally we should return the new context, not mutate the context given in our arguments which is the serializer context + // this would allow to use `Parameter::filterContext` properly, for now let's retrieve it like this: + /** @var MetadataParameter|null */ + $parameter = $request->attributes->get('_api_parameter', null); + $parameterName = $this->parameterName; + $whitelist = $this->whitelist; + $overrideDefaultProperties = $this->overrideDefaultProperties; + + if ($parameter) { + $parameterName = $parameter->getKey(); + $whitelist = $parameter->getFilterContext()['whitelist'] ?? $this->whitelist; + $overrideDefaultProperties = $parameter->getFilterContext()['override_default_properties'] ?? $this->overrideDefaultProperties; + } + if (null !== $propertyAttribute = $request->attributes->get('_api_filter_property')) { $properties = $propertyAttribute; - } elseif (\array_key_exists($this->parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { - $properties = $commonAttribute[$this->parameterName]; + } elseif (\array_key_exists($parameterName, $commonAttribute = $request->attributes->get('_api_filters', []))) { + $properties = $commonAttribute[$parameterName]; } else { - $properties = $request->query->all()[$this->parameterName] ?? null; + $properties = $request->query->all()[$parameterName] ?? null; } if (!\is_array($properties)) { return; } + // TODO: when refactoring this eventually, note that the ParameterResourceMetadataCollectionFactory already does that and caches this behavior in our Parameter metadata $properties = $this->denormalizeProperties($properties); - if (null !== $this->whitelist) { - $properties = $this->getProperties($properties, $this->whitelist); + if (null !== $whitelist) { + $properties = $this->getProperties($properties, $whitelist); } - if (!$this->overrideDefaultProperties && isset($context[AbstractNormalizer::ATTRIBUTES])) { + if (!$overrideDefaultProperties && isset($context[AbstractNormalizer::ATTRIBUTES])) { $properties = array_merge_recursive((array) $context[AbstractNormalizer::ATTRIBUTES], $properties); } @@ -157,7 +177,8 @@ public function apply(Request $request, bool $normalization, array $attributes, */ public function getDescription(string $resourceClass): array { - $example = \sprintf('%1$s[]={propertyName}&%1$s[]={anotherPropertyName}&%1$s[{nestedPropertyParent}][]={nestedProperty}', + $example = \sprintf( + '%1$s[]={propertyName}&%1$s[]={anotherPropertyName}&%1$s[{nestedPropertyParent}][]={nestedProperty}', $this->parameterName ); @@ -167,24 +188,17 @@ public function getDescription(string $resourceClass): array 'is_collection' => true, 'required' => false, 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example, - 'swagger' => [ - 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example, - 'name' => "$this->parameterName[]", - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'openapi' => [ - 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example, - 'name' => "$this->parameterName[]", - 'schema' => [ + 'openapi' => new Parameter( + in: 'query', + name: "$this->parameterName[]", + description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example, + schema: [ 'type' => 'array', 'items' => [ 'type' => 'string', ], - ], - ], + ] + ), ], ]; } @@ -252,4 +266,28 @@ private function denormalizePropertyName($property): string { return null !== $this->nameConverter ? $this->nameConverter->denormalize($property) : $property; } + + public function getSchema(MetadataParameter $parameter): array + { + return [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ]; + } + + public function getOpenApiParameters(MetadataParameter $parameter): Parameter|array|null + { + $example = \sprintf( + '%1$s[]={propertyName}&%1$s[]={anotherPropertyName}', + $parameter->getKey() + ); + + return new Parameter( + name: $parameter->getKey().'[]', + in: $parameter instanceof QueryParameter ? 'query' : 'header', + description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.$example + ); + } } diff --git a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php new file mode 100644 index 00000000000..87d144a7a6f --- /dev/null +++ b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Mapping\Loader; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Serializer\Attribute\Context; +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Attribute\MaxDepth; +use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Serializer\Attribute\SerializedPath; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; + +/** + * Loader for PHP attributes using ApiProperty. + */ +final class PropertyMetadataLoader implements LoaderInterface +{ + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory) + { + } + + public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool + { + $attributesMetadata = $classMetadata->getAttributesMetadata(); + // It's very weird to grab Eloquent's properties in that case as they're never serialized + // the Serializer makes a call on the abstract class, let's save some unneeded work with a condition + if (Model::class === $classMetadata->getName()) { + return false; + } + + $refl = $classMetadata->getReflectionClass(); + $attributes = []; + $classGroups = []; + $classContextAnnotation = null; + + foreach ($refl->getAttributes(ApiProperty::class) as $clAttr) { + $this->addAttributeMetadata($clAttr->newInstance(), $attributes); + } + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + + foreach ($refl->getAttributes() as $a) { + $attribute = $a->newInstance(); + if ($attribute instanceof DiscriminatorMap) { + $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( + $attribute->getTypeProperty(), + $attribute->getMapping() + )); + continue; + } + + if ($attribute instanceof Groups) { + $classGroups = $attribute->getGroups(); + + continue; + } + + if ($attribute instanceof Context) { + $classContextAnnotation = $attribute; + } + } + + foreach ($refl->getProperties() as $reflProperty) { + foreach ($reflProperty->getAttributes(ApiProperty::class) as $propAttr) { + $this->addAttributeMetadata($propAttr->newInstance()->withProperty($reflProperty->name), $attributes); + } + } + + foreach ($refl->getMethods() as $reflMethod) { + foreach ($reflMethod->getAttributes(ApiProperty::class) as $methodAttr) { + $this->addAttributeMetadata($methodAttr->newInstance()->withProperty($reflMethod->getName()), $attributes); + } + } + + foreach ($this->propertyNameCollectionFactory->create($classMetadata->getName()) as $propertyName) { + if (!isset($attributesMetadata[$propertyName])) { + $attributesMetadata[$propertyName] = new AttributeMetadata($propertyName); + $classMetadata->addAttributeMetadata($attributesMetadata[$propertyName]); + } + + foreach ($classGroups as $group) { + $attributesMetadata[$propertyName]->addGroup($group); + } + + if ($classContextAnnotation) { + $this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$propertyName]); + } + + if (!isset($attributes[$propertyName])) { + continue; + } + + $attributeMetadata = $attributesMetadata[$propertyName]; + + // This code is adapted from Symfony\Component\Serializer\Mapping\Loader\AttributeLoader + foreach ($attributes[$propertyName] as $attr) { + if ($attr instanceof Groups) { + foreach ($attr->getGroups() as $group) { + $attributeMetadata->addGroup($group); + } + continue; + } + + match (true) { + $attr instanceof MaxDepth => $attributeMetadata->setMaxDepth($attr->getMaxDepth()), + $attr instanceof SerializedName => $attributeMetadata->setSerializedName($attr->getSerializedName()), + $attr instanceof SerializedPath => $attributeMetadata->setSerializedPath($attr->getSerializedPath()), + $attr instanceof Ignore => $attributeMetadata->setIgnore(true), + $attr instanceof Context => $this->setAttributeContextsForGroups($attr, $attributeMetadata), + default => null, + }; + } + } + + return true; + } + + /** + * @param ApiProperty[] $attributes + */ + private function addAttributeMetadata(ApiProperty $attribute, array &$attributes): void + { + if (($prop = $attribute->getProperty()) && ($value = $attribute->getSerialize())) { + $attributes[$prop] = $value; + } + } + + private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void + { + $context = $annotation->getContext(); + $groups = $annotation->getGroups(); + $normalizationContext = $annotation->getNormalizationContext(); + $denormalizationContext = $annotation->getDenormalizationContext(); + if ($normalizationContext || $context) { + $attributeMetadata->setNormalizationContextForGroups($normalizationContext ?: $context, $groups); + } + + if ($denormalizationContext || $context) { + $attributeMetadata->setDenormalizationContextForGroups($denormalizationContext ?: $context, $groups); + } + } +} diff --git a/src/Serializer/Parameter/SerializerFilterParameterProvider.php b/src/Serializer/Parameter/SerializerFilterParameterProvider.php index 59bd3fe83a0..dfabc85de1c 100644 --- a/src/Serializer/Parameter/SerializerFilterParameterProvider.php +++ b/src/Serializer/Parameter/SerializerFilterParameterProvider.php @@ -45,7 +45,7 @@ public function provide(Parameter $parameter, array $parameters = [], array $con return null; } - $context = $operation->getNormalizationContext(); + $context = $operation->getNormalizationContext() ?? []; $request->attributes->set('_api_parameter', $parameter); $filter->apply($request, true, RequestAttributesExtractor::extractAttributes($request), $context); diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php index f9aa34c1bf1..1393a3c07b8 100644 --- a/src/Serializer/SerializerContextBuilder.php +++ b/src/Serializer/SerializerContextBuilder.php @@ -19,6 +19,7 @@ use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Util\AttributesExtractor; +use ApiPlatform\State\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; diff --git a/src/Serializer/SerializerContextBuilderInterface.php b/src/Serializer/SerializerContextBuilderInterface.php deleted file mode 100644 index dbae714fd7d..00000000000 --- a/src/Serializer/SerializerContextBuilderInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Serializer; - -use ApiPlatform\State\SerializerContextBuilderInterface as StateSerializerContextBuilderInterface; - -/** - * Builds the context used by the Symfony Serializer. - * - * @deprecated use ApiPlatform\State\SerializerContextBuilderInterface instead - * - * @author Kévin Dunglas - */ -interface SerializerContextBuilderInterface extends StateSerializerContextBuilderInterface -{ -} diff --git a/src/Serializer/SerializerFilterContextBuilder.php b/src/Serializer/SerializerFilterContextBuilder.php index 9073bbe1136..3cf9cb5894b 100644 --- a/src/Serializer/SerializerFilterContextBuilder.php +++ b/src/Serializer/SerializerFilterContextBuilder.php @@ -17,7 +17,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Util\AttributesExtractor; use ApiPlatform\Serializer\Filter\FilterInterface; -use ApiPlatform\State\SerializerContextBuilderInterface as StateSerializerContextBuilderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -28,7 +28,7 @@ */ final class SerializerFilterContextBuilder implements SerializerContextBuilderInterface { - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ContainerInterface $filterLocator, private readonly SerializerContextBuilderInterface|StateSerializerContextBuilderInterface $decorated) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ContainerInterface $filterLocator, private readonly SerializerContextBuilderInterface $decorated) { } diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 95a7f1d8246..b469d436923 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -43,7 +43,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; @@ -60,12 +59,9 @@ */ class AbstractItemNormalizerTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testSupportNormalizationAndSupportDenormalization(): void { $std = new \stdClass(); @@ -80,18 +76,7 @@ public function testSupportNormalizationAndSupportDenormalization(): void $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $this->assertTrue($normalizer->supportsNormalization($dummy)); $this->assertFalse($normalizer->supportsNormalization($std)); @@ -99,10 +84,6 @@ public function testSupportNormalizationAndSupportDenormalization(): void $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class)); $this->assertFalse($normalizer->supportsNormalization([])); $this->assertSame(['object' => true], $normalizer->getSupportedTypes('any')); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalize(): void @@ -151,18 +132,7 @@ public function testNormalize(): void $serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo'); $serializerProphecy->normalize(['/dummies/2'], null, Argument::type('array'))->willReturn(['/dummies/2']); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $expected = [ @@ -212,18 +182,7 @@ public function testNormalizeWithSecuredProperty(): void $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('myPublicTitle', null, Argument::type('array'))->willReturn('myPublicTitle'); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $expected = [ @@ -295,18 +254,8 @@ public function testNormalizePropertyAsIriWithUriTemplate(): void $resourceClassResolverProphecy->isResourceClass(PropertyCollectionIriOnlyRelation::class)->willReturn(true); $resourceClassResolverProphecy->getResourceClass([$propertyCollectionIriOnlyRelation], PropertyCollectionIriOnlyRelation::class)->willReturn(PropertyCollectionIriOnlyRelation::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - new PropertyAccessor(), // $propertyAccessorProphecy->reveal(), - null, - null, - [], - $resourceMetadataCollectionFactoryProphecy->reveal(), - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), new PropertyAccessor(), // $propertyAccessorProphecy->reveal(), + null, null, [], $resourceMetadataCollectionFactoryProphecy->reveal(), null, ) extends AbstractItemNormalizer {}; $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); @@ -356,18 +305,7 @@ public function testDenormalizeWithSecuredProperty(): void ['object' => null, 'property' => 'adminOnlyProperty'] )->willReturn(false); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, SecuredDummy::class); @@ -410,18 +348,7 @@ public function testDenormalizeCreateWithDeniedPostDenormalizeSecuredProperty(): Argument::any() )->willReturn(false); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, SecuredDummy::class); @@ -473,18 +400,7 @@ public function testDenormalizeUpdateWithSecuredProperty(): void ['object' => $dummy, 'property' => 'ownerOnlyProperty'] )->willReturn(true); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; @@ -537,18 +453,7 @@ public function testDenormalizeUpdateWithDeniedSecuredProperty(): void ['object' => $dummy, 'property' => 'ownerOnlyProperty'] )->willReturn(false); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; @@ -597,18 +502,7 @@ public function testDenormalizeUpdateWithDeniedPostDenormalizeSecuredProperty(): ['object' => $dummy, 'previous_object' => $dummy, 'property' => 'ownerOnlyProperty'] )->willReturn(false); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - $resourceAccessChecker->reveal(), - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; @@ -668,18 +562,7 @@ public function testNormalizeReadableLinks(): void $serializerProphecy->normalize(['foo' => 'hello'], null, Argument::type('array'))->willReturn(['foo' => 'hello']); $serializerProphecy->normalize([['foo' => 'hello']], null, Argument::type('array'))->willReturn([['foo' => 'hello']]); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $expected = [ @@ -733,18 +616,7 @@ public function testNormalizePolymorphicRelations(): void $serializerProphecy->normalize($concreteDummy, null, $concreteDummyChildContext)->willReturn(['foo' => 'concrete']); $serializerProphecy->normalize([['foo' => 'concrete']], null, Argument::type('array'))->willReturn([['foo' => 'concrete']]); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $expected = [ @@ -793,18 +665,7 @@ public function testDenormalize(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, Dummy::class); @@ -918,18 +779,7 @@ public function testDenormalizeWritableLinks(): void $serializerProphecy->denormalize(['bar' => 'qux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy3); $serializerProphecy->denormalize(['bar' => 'quux'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy4); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, Dummy::class); @@ -972,18 +822,7 @@ public function testBadRelationType(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize($data, Dummy::class); @@ -1016,18 +855,7 @@ public function testBadRelationTypeWithExceptionToValidationErrors(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); // 'not_normalizable_value_exceptions' is set by Serializer thanks to DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS @@ -1117,18 +945,7 @@ public function testInnerDocumentNotAllowed(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize($data, Dummy::class); @@ -1160,18 +977,7 @@ public function testBadType(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize($data, Dummy::class); @@ -1200,18 +1006,7 @@ public function testTypeChecksCanBeDisabled(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, Dummy::class, null, ['disable_type_enforcement' => true]); @@ -1244,18 +1039,7 @@ public function testJsonAllowIntAsFloat(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, Dummy::class, 'jsonfoo'); @@ -1317,18 +1101,7 @@ public function testDenormalizeBadKeyType(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize($data, Dummy::class); @@ -1357,18 +1130,7 @@ public function testNullable(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $actual = $normalizer->denormalize($data, Dummy::class); @@ -1433,18 +1195,7 @@ public function testDenormalizeBasicTypePropertiesFromXml(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $objectWithBasicProperties = $normalizer->denormalize( @@ -1504,18 +1255,7 @@ public function testDenormalizeCollectionDecodedFromXmlWithOneChild(): void $serializerProphecy->willImplement(DenormalizerInterface::class); $serializerProphecy->denormalize(['name' => 'foo'], RelatedDummy::class, 'xml', Argument::type('array'))->willReturn($relatedDummy); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize($data, Dummy::class, 'xml'); @@ -1546,18 +1286,7 @@ public function testDenormalizePopulatingNonCloneableObject(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -1590,18 +1319,7 @@ public function testDenormalizeObjectWithNullDisabledTypeEnforcement(): void $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $context = [AbstractItemNormalizer::DISABLE_TYPE_ENFORCEMENT => true]; @@ -1657,18 +1375,7 @@ public function testCacheKey(): void $serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo'); $serializerProphecy->normalize(['/dummies/2'], null, Argument::type('array'))->willReturn(['/dummies/2']); - $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $propertyAccessorProphecy->reveal(), - null, - null, - [], - null, - null, - ]); + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); $expected = [ diff --git a/src/Serializer/Tests/Filter/PropertyFilterTest.php b/src/Serializer/Tests/Filter/PropertyFilterTest.php index 407aa53c2c5..0c90f3836da 100644 --- a/src/Serializer/Tests/Filter/PropertyFilterTest.php +++ b/src/Serializer/Tests/Filter/PropertyFilterTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Serializer\Tests\Filter; +use ApiPlatform\OpenApi\Model\Parameter; use ApiPlatform\Serializer\Filter\PropertyFilter; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyProperty; use ApiPlatform\Serializer\Tests\Fixtures\Serializer\NameConverter\CustomConverter; @@ -218,27 +219,20 @@ public function testGetDescription(): void 'is_collection' => true, 'required' => false, 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', - 'swagger' => [ - 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', - 'name' => 'custom_properties[]', - 'type' => 'array', - 'items' => [ - 'type' => 'string', - ], - ], - 'openapi' => [ - 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', - 'name' => 'custom_properties[]', - 'schema' => [ + 'openapi' => new Parameter( + in: 'query', + description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', + name: 'custom_properties[]', + schema: [ 'type' => 'array', 'items' => [ 'type' => 'string', ], - ], - ], + ] + ), ], ]; - $this->assertSame($expectedDescription, $propertyFilter->getDescription(DummyProperty::class)); + $this->assertEquals($expectedDescription, $propertyFilter->getDescription(DummyProperty::class)); } } diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.php b/src/Serializer/Tests/Fixtures/Model/HasRelation.php similarity index 54% rename from tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.php rename to src/Serializer/Tests/Fixtures/Model/HasRelation.php index 59d70e6e924..833fee55495 100644 --- a/tests/Fixtures/TestBundle/ApiResource/Issue5896/Foo.php +++ b/src/Serializer/Tests/Fixtures/Model/HasRelation.php @@ -11,15 +11,16 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896; +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\Get; +use Symfony\Component\Serializer\Attribute\Groups; -#[Get] -class Foo +class HasRelation { - #[ApiProperty(readable: false, writable: false, identifier: true)] - public ?int $id = null; - public ?LocalDate $expiration; + #[ApiProperty(serialize: [new Groups(['read'])])] + public function relation(): Relation + { + return new Relation(); + } } diff --git a/src/Serializer/Tests/Fixtures/Model/Relation.php b/src/Serializer/Tests/Fixtures/Model/Relation.php new file mode 100644 index 00000000000..4d06da7d1b8 --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/Relation.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +use Symfony\Component\Serializer\Attribute\Groups; + +#[Groups(['read'])] +class Relation +{ +} diff --git a/src/Serializer/Tests/ItemNormalizerTest.php b/src/Serializer/Tests/ItemNormalizerTest.php index 0d59c6096cb..6028b7d68a8 100644 --- a/src/Serializer/Tests/ItemNormalizerTest.php +++ b/src/Serializer/Tests/ItemNormalizerTest.php @@ -31,11 +31,9 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; /** @@ -43,12 +41,8 @@ */ class ItemNormalizerTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; - /** - * @group legacy - */ public function testSupportNormalization(): void { $std = new \stdClass(); @@ -78,10 +72,6 @@ public function testSupportNormalization(): void $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class)); $this->assertSame(['object' => true], $normalizer->getSupportedTypes('any')); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalize(): void diff --git a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php new file mode 100644 index 00000000000..27a8fc55936 --- /dev/null +++ b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Mapping\Loader; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; +use ApiPlatform\Serializer\Tests\Fixtures\Model\HasRelation; +use ApiPlatform\Serializer\Tests\Fixtures\Model\Relation; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\ClassMetadata; + +final class PropertyMetadataLoaderTest extends TestCase +{ + public function testCreateMappingForASetOfProperties(): void + { + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection(['relation'])); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata(HasRelation::class); + $loader->loadClassMetadata($classMetadata); + $this->assertArrayHasKey('relation', $classMetadata->attributesMetadata); + $this->assertEquals(['read'], $classMetadata->attributesMetadata['relation']->getGroups()); + } + + public function testCreateMappingForAClass(): void + { + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection(['name'])); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata(Relation::class); + $loader->loadClassMetadata($classMetadata); + $this->assertArrayHasKey('name', $classMetadata->attributesMetadata); + $this->assertEquals(['read'], $classMetadata->attributesMetadata['name']->getGroups()); + } +} diff --git a/src/Serializer/Tests/SerializerFilterContextBuilderTest.php b/src/Serializer/Tests/SerializerFilterContextBuilderTest.php index a06b8ce34f7..41b7da79a94 100644 --- a/src/Serializer/Tests/SerializerFilterContextBuilderTest.php +++ b/src/Serializer/Tests/SerializerFilterContextBuilderTest.php @@ -20,9 +20,9 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; use ApiPlatform\Serializer\SerializerFilterContextBuilder; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyGroup; +use ApiPlatform\State\SerializerContextBuilderInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; diff --git a/src/Serializer/composer.json b/src/Serializer/composer.json index 3b7df7d51e0..51a7cbf5aec 100644 --- a/src/Serializer/composer.json +++ b/src/Serializer/composer.json @@ -22,20 +22,24 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "api-platform/state": "^3.4 || ^4.0", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/validator": "^6.4 || ^7.0" }, "require-dev": { + "api-platform/doctrine-common": "^3.4 || ^4.0", + "api-platform/doctrine-odm": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", + "api-platform/json-schema": "^3.4 || ^4.0", + "api-platform/openapi": "^3.4 || ^4.0", "doctrine/collections": "^2.1", - "phpspec/prophecy-phpunit": "^2.0", - "sebastian/comparator": "<5.0", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^11.2", "symfony/mercure-bundle": "*", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/var-dumper": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0" }, @@ -67,7 +71,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Serializer/phpunit.xml.dist b/src/Serializer/phpunit.xml.dist index 47576646e09..56504cf1361 100644 --- a/src/Serializer/phpunit.xml.dist +++ b/src/Serializer/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + diff --git a/src/State/ApiResource/Error.php b/src/State/ApiResource/Error.php index 2ad7ebfa653..ec7e816824f 100644 --- a/src/State/ApiResource/Error.php +++ b/src/State/ApiResource/Error.php @@ -36,7 +36,6 @@ normalizationContext: [ 'groups' => ['jsonproblem'], 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, ], ), new Operation( @@ -46,7 +45,6 @@ normalizationContext: [ 'groups' => ['jsonld'], 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, ], links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')], ), @@ -57,7 +55,6 @@ normalizationContext: [ 'groups' => ['jsonapi'], 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, ], ), new Operation( @@ -93,6 +90,12 @@ public function __construct( } } + #[Groups(['jsonapi'])] + public function getId(): string + { + return (string) $this->status; + } + #[SerializedName('trace')] #[Groups(['trace'])] public ?array $originalTrace = null; @@ -132,7 +135,7 @@ public function setHeaders(array $headers): void $this->headers = $headers; } - #[Groups(['jsonld', 'jsonproblem'])] + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] public function getType(): string { return $this->type; diff --git a/src/State/Processor/RespondProcessor.php b/src/State/Processor/RespondProcessor.php index 15e9c030327..64b07308d3f 100644 --- a/src/State/Processor/RespondProcessor.php +++ b/src/State/Processor/RespondProcessor.php @@ -14,6 +14,9 @@ namespace ApiPlatform\State\Processor; use ApiPlatform\Metadata\Exception\HttpExceptionInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; @@ -92,18 +95,23 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $hasOutput = \is_array($outputMetadata) && \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class']; $hasData = !$hasOutput ? false : ($this->resourceClassResolver && $originalData && \is_object($originalData) && $this->resourceClassResolver->isResourceClass($this->getObjectClass($originalData))); - if ($hasData && $this->iriConverter) { + if ($hasData) { + $isAlternateResourceMetadata = $operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false; + $canonicalUriTemplate = $operation->getExtraProperties()['canonical_uri_template'] ?? null; + if ( !isset($headers['Location']) && 300 <= $status && $status < 400 - && (($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false) || ($operation->getExtraProperties()['canonical_uri_template'] ?? null)) + && ($isAlternateResourceMetadata || $canonicalUriTemplate) ) { $canonicalOperation = $operation; - if ($this->operationMetadataFactory && null !== ($operation->getExtraProperties()['canonical_uri_template'] ?? null)) { - $canonicalOperation = $this->operationMetadataFactory->create($operation->getExtraProperties()['canonical_uri_template'], $context); + if ($this->operationMetadataFactory && null !== $canonicalUriTemplate) { + $canonicalOperation = $this->operationMetadataFactory->create($canonicalUriTemplate, $context); } - $headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $canonicalOperation); + if ($this->iriConverter) { + $headers['Location'] = $this->iriConverter->getIriFromResource($originalData, UrlGeneratorInterface::ABS_PATH, $canonicalOperation); + } } elseif ('PUT' === $method && !$request->attributes->get('previous_data') && null === $status && ($operation instanceof Put && ($operation->getAllowCreate() ?? false))) { $status = 201; } @@ -111,12 +119,28 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $status ??= self::METHOD_TO_CODE[$method] ?? 200; - if ($hasData && $this->iriConverter && !isset($headers['Content-Location'])) { - $iri = $this->iriConverter->getIriFromResource($originalData); - $headers['Content-Location'] = $iri; + $requestParts = parse_url($request->getRequestUri()); + if ($this->iriConverter && !isset($headers['Content-Location'])) { + try { + $iri = null; + if ($hasData) { + $iri = $this->iriConverter->getIriFromResource($originalData); + } elseif ($operation->getClass()) { + $iri = $this->iriConverter->getIriFromResource($operation->getClass(), UrlGeneratorInterface::ABS_PATH, $operation); + } - if ((201 === $status || (300 <= $status && $status < 400)) && 'POST' === $method && !isset($headers['Location'])) { - $headers['Location'] = $iri; + if ($iri) { + $location = \sprintf('%s.%s', $iri, $request->getRequestFormat()); + if (isset($requestParts['query'])) { + $location .= '?'.$requestParts['query']; + } + + $headers['Content-Location'] = $location; + if ((201 === $status || (300 <= $status && $status < 400)) && 'POST' === $method && !isset($headers['Location'])) { + $headers['Location'] = $iri; + } + } + } catch (InvalidArgumentException|ItemNotFoundException|RuntimeException) { } } diff --git a/src/State/Processor/SerializeProcessor.php b/src/State/Processor/SerializeProcessor.php index 088eb3e7dec..b56bd332a4d 100644 --- a/src/State/Processor/SerializeProcessor.php +++ b/src/State/Processor/SerializeProcessor.php @@ -52,11 +52,6 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $context['original_data'] = $data; $class = $operation->getClass(); - if ($request->attributes->get('_api_resource_class') && $request->attributes->get('_api_resource_class') !== $operation->getClass()) { - $class = $request->attributes->get('_api_resource_class'); - trigger_deprecation('api-platform/core', '3.3', 'The resource class on the router is not the same as the operation\'s class which leads to wrong behaviors. Prefer using "stateOptions" if you need to change the entity class.'); - } - $serializerContext = $this->serializerContextBuilder->createFromRequest($request, true, [ 'resource_class' => $class, 'operation' => $operation, diff --git a/src/State/ProcessorInterface.php b/src/State/ProcessorInterface.php index b88f77f3716..07bb906798a 100644 --- a/src/State/ProcessorInterface.php +++ b/src/State/ProcessorInterface.php @@ -29,9 +29,9 @@ interface ProcessorInterface /** * Handles the state. * - * @param T1 $data - * @param array $uriVariables - * @param array&array{request?: Request, previous_data?: mixed, resource_class?: string, original_data?: mixed} $context + * @param T1 $data + * @param array $uriVariables + * @param array&array{request?: Request|\Illuminate\Http\Request, previous_data?: mixed, resource_class?: string|null, original_data?: mixed} $context * * @return T2 */ diff --git a/src/State/Provider/ContentNegotiationProvider.php b/src/State/Provider/ContentNegotiationProvider.php index 0ffd42607b6..f01b3e268d0 100644 --- a/src/State/Provider/ContentNegotiationProvider.php +++ b/src/State/Provider/ContentNegotiationProvider.php @@ -46,12 +46,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $formats = $operation->getOutputFormats() ?? ($isErrorOperation ? $this->errorFormats : $this->formats); $this->addRequestFormats($request, $formats); $request->attributes->set('input_format', $this->getInputFormat($operation, $request)); - - if (!$isErrorOperation) { - $request->setRequestFormat($this->getRequestFormat($request, $formats)); - } else { - $request->setRequestFormat($this->getRequestFormat($request, $formats, false)); - } + $request->setRequestFormat($this->getRequestFormat($request, $formats, !$isErrorOperation)); return $this->decorated?->provide($operation, $uriVariables, $context); } diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index 1e8a68310bb..be399f4f1bf 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -15,7 +15,6 @@ use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; -use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\Validator\Exception\ValidationException; @@ -36,7 +35,7 @@ final class DeserializeProvider implements ProviderInterface public function __construct( private readonly ?ProviderInterface $decorated, private readonly SerializerInterface $serializer, - private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface $serializerContextBuilder, + private readonly SerializerContextBuilderInterface $serializerContextBuilder, private ?TranslatorInterface $translator = null, ) { if (null === $this->translator) { @@ -83,7 +82,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c && ( 'POST' === $method || 'PATCH' === $method - || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? false)) + || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true)) ) ) { $serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data; diff --git a/src/State/Provider/ReadProvider.php b/src/State/Provider/ReadProvider.php index b3450dc5a32..f5ecb5510c0 100644 --- a/src/State/Provider/ReadProvider.php +++ b/src/State/Provider/ReadProvider.php @@ -17,7 +17,6 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\Exception\ProviderNotFoundException; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\SerializerContextBuilderInterface; @@ -40,7 +39,7 @@ final class ReadProvider implements ProviderInterface public function __construct( private readonly ProviderInterface $provider, - private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|null $serializerContextBuilder = null, + private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null, private readonly ?LoggerInterface $logger = null, ) { } diff --git a/src/State/Provider/SecurityParameterProvider.php b/src/State/Provider/SecurityParameterProvider.php index 7f06bc785e9..9e9c64906b2 100644 --- a/src/State/Provider/SecurityParameterProvider.php +++ b/src/State/Provider/SecurityParameterProvider.php @@ -49,7 +49,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c continue; } - $securityContext = [$parameter->getKey() => $v, 'object' => $body]; + $securityContext = [$parameter->getKey() => $v, 'object' => $body, 'operation' => $operation]; if (!$this->resourceAccessChecker->isGranted($context['resource_class'], $security, $securityContext)) { throw $operation instanceof GraphQlOperation ? new AccessDeniedHttpException($parameter->getSecurityMessage() ?? 'Access Denied.') : new AccessDeniedException($parameter->getSecurityMessage() ?? 'Access Denied.'); } diff --git a/src/State/SerializerContextBuilderInterface.php b/src/State/SerializerContextBuilderInterface.php index 3bb88bfb5d8..cdbbee16698 100644 --- a/src/State/SerializerContextBuilderInterface.php +++ b/src/State/SerializerContextBuilderInterface.php @@ -49,6 +49,7 @@ interface SerializerContextBuilderInterface * collect_denormalization_errors?: bool, * exclude_from_cache_key?: string[], * api_included?: bool, + * attributes?: string[], * deserializer_type?: string, * } */ diff --git a/src/State/Tests/Util/RequestParserTest.php b/src/State/Tests/Util/RequestParserTest.php index 831aac33a6d..249f5b189b1 100644 --- a/src/State/Tests/Util/RequestParserTest.php +++ b/src/State/Tests/Util/RequestParserTest.php @@ -21,9 +21,7 @@ */ class RequestParserTest extends TestCase { - /** - * @dataProvider parseRequestParamsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('parseRequestParamsProvider')] public function testParseRequestParams(string $source, array $expected): void { $actual = RequestParser::parseRequestParams($source); diff --git a/src/State/UriVariablesResolverTrait.php b/src/State/UriVariablesResolverTrait.php index 68b2d060703..0463041b1f2 100644 --- a/src/State/UriVariablesResolverTrait.php +++ b/src/State/UriVariablesResolverTrait.php @@ -13,7 +13,6 @@ namespace ApiPlatform\State; -use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Metadata\Exception\InvalidIdentifierException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\UriVariablesConverterInterface; @@ -21,7 +20,7 @@ trait UriVariablesResolverTrait { - private LegacyUriVariablesConverterInterface|UriVariablesConverterInterface|null $uriVariablesConverter = null; + private ?UriVariablesConverterInterface $uriVariablesConverter = null; /** * Resolves an operation's UriVariables to their identifiers values. diff --git a/src/State/Util/OperationRequestInitiatorTrait.php b/src/State/Util/OperationRequestInitiatorTrait.php index b70e9d271ca..4261ece85d3 100644 --- a/src/State/Util/OperationRequestInitiatorTrait.php +++ b/src/State/Util/OperationRequestInitiatorTrait.php @@ -38,6 +38,7 @@ private function initializeOperation(Request $request): ?HttpOperation } $operationName = $request->attributes->get('_api_operation_name'); + /** @var HttpOperation $operation */ $operation = $this->resourceMetadataCollectionFactory->create($request->attributes->get('_api_resource_class'))->getOperation($operationName); $request->attributes->set('_api_operation', $operation); diff --git a/src/State/Util/ParameterParserTrait.php b/src/State/Util/ParameterParserTrait.php index 6db86bfa463..cadbbcb8eb7 100644 --- a/src/State/Util/ParameterParserTrait.php +++ b/src/State/Util/ParameterParserTrait.php @@ -46,6 +46,10 @@ private function extractParameterValues(Parameter $parameter, array $values): st { $accessors = null; $key = $parameter->getKey(); + if (null === $key) { + throw new \RuntimeException('A Parameter should have a key.'); + } + $parsedKey = explode('[:property]', $key); if (isset($parsedKey[0]) && isset($values[$parsedKey[0]])) { $key = $parsedKey[0]; diff --git a/src/State/composer.json b/src/State/composer.json index c813028d6fb..72664396851 100644 --- a/src/State/composer.json +++ b/src/State/composer.json @@ -27,13 +27,13 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", "psr/container": "^1.0 || ^2.0", "symfony/http-kernel": "^6.4 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^10.3", + "phpunit/phpunit": "^11.2", "symfony/web-link": "^6.4 || ^7.0", "symfony/http-foundation": "^6.4 || ^7.0", "willdurand/negotiation": "^3.1", @@ -64,7 +64,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index cd5d08e9741..f7fc270f085 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -23,6 +23,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -58,5 +59,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestClientPass()); $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); + $container->addCompilerPass(new SerializerMappingLoaderPass()); } } diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index 72f8ad2b64c..ea359270c10 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -16,7 +16,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index f8f233fa699..5c83b24b099 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Symfony\Bundle\DependencyInjection; -use ApiPlatform\Api\FilterInterface as LegacyFilterInterface; -use ApiPlatform\Api\QueryParameterValidator\QueryParameterValidator; use ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface; use ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface; use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter as DoctrineMongoDbOdmAbstractFilter; @@ -32,23 +30,15 @@ use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; -use ApiPlatform\Hydra\EventListener\AddLinkHeaderListener as HydraAddLinkHeaderListener; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\UriVariableTransformerInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Problem\Serializer\ConstraintViolationListNormalizer; use ApiPlatform\RamseyUuid\Serializer\UuidDenormalizer; use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\ParameterProviderInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; -use ApiPlatform\Symfony\EventListener\AddHeadersListener; -use ApiPlatform\Symfony\EventListener\AddLinkHeaderListener; -use ApiPlatform\Symfony\EventListener\AddTagsListener; -use ApiPlatform\Symfony\EventListener\DenyAccessListener; -use ApiPlatform\Symfony\GraphQl\Resolver\Factory\DataCollectorResolverFactory; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as SymfonyValidationException; use ApiPlatform\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; use ApiPlatform\Validator\Exception\ValidationException; @@ -59,7 +49,6 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -116,23 +105,8 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - - if (null === $config['use_symfony_listeners']) { - $config['use_symfony_listeners'] = true; - trigger_deprecation('api-platform/core', '3.3', 'Setting the value of "use_symfony_listeners" will be mandatory in 4.0 as it will default to "false". Use "true" if you use Symfony Controllers or Event Listeners.'); - } - $container->setParameter('api_platform.use_symfony_listeners', $config['use_symfony_listeners']); - if (!$config['formats']) { - trigger_deprecation('api-platform/core', '3.2', 'Setting the "formats" section will be mandatory in API Platform 4.'); - $config['formats'] = [ - 'jsonld' => ['mime_types' => ['application/ld+json']], - // Note that in API Platform 4 this will be removed as it was used for documentation only and are is now present in the docsFormats - 'json' => ['mime_types' => ['application/json']], // Swagger support - ]; - } - $formats = $this->getFormats($config['formats']); $patchFormats = $this->getFormats($config['patch_formats']); $errorFormats = $this->getFormats($config['error_formats']); @@ -156,21 +130,10 @@ public function load(array $configs, ContainerBuilder $container): void $errorFormats['jsonproblem'] = ['application/problem+json']; } - if ($this->isConfigEnabled($container, $config['graphql']) && !isset($formats['json'])) { - trigger_deprecation('api-platform/core', '3.2', 'Add the "json" format to the configuration to use GraphQL.'); - $formats['json'] = ['application/json']; - } - - // Backward Compatibility layer if (isset($formats['jsonapi']) && !isset($patchFormats['jsonapi'])) { $patchFormats['jsonapi'] = ['application/vnd.api+json']; } - if (isset($docsFormats['json']) && !isset($docsFormats['jsonopenapi'])) { - trigger_deprecation('api-platform/core', '3.2', 'The "json" format is too broad, use ["jsonopenapi" => ["application/vnd.openapi+json"]] instead.'); - $docsFormats['jsonopenapi'] = ['application/vnd.openapi+json']; - } - $this->registerCommonConfiguration($container, $config, $loader, $formats, $patchFormats, $errorFormats, $docsFormats, $jsonSchemaFormats); $this->registerMetadataConfiguration($container, $config, $loader); $this->registerOAuthConfiguration($container, $config); @@ -197,8 +160,6 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(FilterInterface::class) ->addTag('api_platform.filter'); - $container->registerForAutoconfiguration(LegacyFilterInterface::class) - ->addTag('api_platform.filter'); $container->registerForAutoconfiguration(ProviderInterface::class) ->addTag('api_platform.state_provider'); $container->registerForAutoconfiguration(ProcessorInterface::class) @@ -211,8 +172,6 @@ public function load(array $configs, ContainerBuilder $container): void if (!$container->has('api_platform.state.item_provider')) { $container->setAlias('api_platform.state.item_provider', 'api_platform.state_provider.object'); } - - $this->registerInflectorConfiguration($container, $config); } private function registerCommonConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader, array $formats, array $patchFormats, array $errorFormats, array $docsFormats, array $jsonSchemaFormats): void @@ -230,35 +189,12 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $loader->load('symfony/uid.xml'); } - // TODO: remove in 4.x - $container->setParameter('api_platform.event_listeners_backward_compatibility_layer', $config['event_listeners_backward_compatibility_layer']); - $defaultContext = ['hydra_prefix' => $config['serializer']['hydra_prefix']] + ($container->hasParameter('serializer.default_context') ? $container->getParameter('serializer.default_context') : []); - if (null === $defaultContext['hydra_prefix']) { - trigger_deprecation('api-platform/core', '3.3', 'The hydra: prefix will be removed in 4.0 by default, set "api_platform.serializer" or "serializer.default_context" to "hydra_prefix: true" to keep the current behavior.'); - $defaultContext['hydra_prefix'] = true; - } - $container->setParameter('api_platform.serializer.default_context', $defaultContext); if (!$container->hasParameter('serializer.default_context')) { $container->setParameter('serializer.default_context', $container->getParameter('api_platform.serializer.default_context')); } - - if ($config['event_listeners_backward_compatibility_layer']) { - trigger_deprecation('api-platform/core', '3.3', \sprintf('The "event_listeners_backward_compatibility_layer" will be removed in 4.0. Use the configuration "use_symfony_listeners" to use Symfony listeners. The following listeners are deprecated and will be removed in API Platform 4.0: "%s"', implode(', ', [ - AddHeadersListener::class, - AddTagsListener::class, - AddLinkHeaderListener::class, - HydraAddLinkHeaderListener::class, - DenyAccessListener::class, - ]))); - } - - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/events.xml'); - } - if ($config['use_symfony_listeners']) { $loader->load('symfony/events.xml'); } else { @@ -269,7 +205,6 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_entrypoint', $config['enable_entrypoint']); $container->setParameter('api_platform.enable_docs', $config['enable_docs']); - $container->setParameter('api_platform.keep_legacy_inflector', $config['keep_legacy_inflector']); $container->setParameter('api_platform.title', $config['title']); $container->setParameter('api_platform.description', $config['description']); $container->setParameter('api_platform.version', $config['version']); @@ -318,7 +253,6 @@ private function registerCommonConfiguration(ContainerBuilder $container, array } $container->setParameter('api_platform.asset_package', $config['asset_package']); $container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? [])); - $container->setParameter('api_platform.rfc_7807_compliant_errors', $config['defaults']['extra_properties']['rfc_7807_compliant_errors'] ?? false); if ($container->getParameter('kernel.debug')) { $container->removeDefinition('api_platform.serializer.mapping.cache_class_metadata_factory'); @@ -409,7 +343,6 @@ private function getClassNameResources(): array { return [ Error::class, - SymfonyValidationException::class, ValidationException::class, ]; } @@ -545,20 +478,12 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $loader->load('openapi.xml'); - if ($config['use_deprecated_json_schema_type_factory'] ?? false) { - $container->getDefinition('api_platform.openapi.factory')->setArgument('$jsonSchemaTypeFactory', new Reference('api_platform.json_schema.type_factory')); - } - if (class_exists(Yaml::class)) { $loader->load('openapi/yaml.xml'); } $loader->load('swagger_ui.xml'); - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/swagger_ui.xml'); - } - if ($config['use_symfony_listeners']) { $loader->load('symfony/swagger_ui.xml'); } @@ -587,10 +512,6 @@ private function registerJsonApiConfiguration(array $formats, XmlFileLoader $loa return; } - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/jsonapi.xml'); - } - $loader->load('jsonapi.xml'); $loader->load('state/jsonapi.xml'); } @@ -607,10 +528,6 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a $loader->load('state/jsonld.xml'); } - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/hydra.xml'); - } - $loader->load('state/hydra.xml'); $loader->load('jsonld.xml'); $loader->load('hydra.xml'); @@ -640,10 +557,6 @@ private function registerJsonProblemConfiguration(array $errorFormats, XmlFileLo return; } - if (class_exists(ConstraintViolationListNormalizer::class)) { - $loader->load('legacy/problem.xml'); - } - $loader->load('problem.xml'); } @@ -696,41 +609,6 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array ->addTag('api_platform.graphql.type'); $container->registerForAutoconfiguration(ErrorHandlerInterface::class) ->addTag('api_platform.graphql.error_handler'); - - /* TODO: remove these in 4.x only one resolver factory is used and we're using providers/processors */ - if ($config['event_listeners_backward_compatibility_layer']) { - // @TODO: API Platform 3.3 trigger_deprecation('api-platform/core', '3.3', 'In API Platform 4 only one factory "api_platform.graphql.resolver.factory.item" will remain. Stages are deprecated in favor of using a provider/processor.'); - // + deprecate every service from legacy/graphql.xml - $loader->load('legacy/graphql.xml'); - - if (!$container->getParameter('kernel.debug')) { - return; - } - - $requestStack = new Reference('request_stack', ContainerInterface::NULL_ON_INVALID_REFERENCE); - $collectionDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) - ->setDecoratedService('api_platform.graphql.resolver.factory.collection') - ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.collection.inner'), $requestStack]); - - $itemDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) - ->setDecoratedService('api_platform.graphql.resolver.factory.item') - ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item.inner'), $requestStack]); - - $itemMutationDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) - ->setDecoratedService('api_platform.graphql.resolver.factory.item_mutation') - ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item_mutation.inner'), $requestStack]); - - $itemSubscriptionDataCollectorResolverFactory = (new Definition(DataCollectorResolverFactory::class)) - ->setDecoratedService('api_platform.graphql.resolver.factory.item_subscription') - ->setArguments([new Reference('api_platform.graphql.data_collector.resolver.factory.item_subscription.inner'), $requestStack]); - - $container->addDefinitions([ - 'api_platform.graphql.data_collector.resolver.factory.collection' => $collectionDataCollectorResolverFactory, - 'api_platform.graphql.data_collector.resolver.factory.item' => $itemDataCollectorResolverFactory, - 'api_platform.graphql.data_collector.resolver.factory.item_mutation' => $itemMutationDataCollectorResolverFactory, - 'api_platform.graphql.data_collector.resolver.factory.item_subscription' => $itemSubscriptionDataCollectorResolverFactory, - ]); - } } private function registerCacheConfiguration(ContainerBuilder $container): void @@ -794,10 +672,6 @@ private function registerHttpCacheConfiguration(ContainerBuilder $container, arr { $loader->load('http_cache.xml'); - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/http_cache.xml'); - } - if (!$this->isConfigEnabled($container, $config['http_cache']['invalidation'])) { return; } @@ -806,10 +680,6 @@ private function registerHttpCacheConfiguration(ContainerBuilder $container, arr $loader->load('doctrine_orm_http_cache_purger.xml'); } - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/http_cache_purger.xml'); - } - $loader->load('state/http_cache_purger.xml'); $loader->load('http_cache_purger.xml'); @@ -854,7 +724,6 @@ private function getFormats(array $configFormats): array private function registerValidatorConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void { if (interface_exists(ValidatorInterface::class)) { - $container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true); $loader->load('metadata/validator.xml'); $loader->load('validator/validator.xml'); @@ -862,10 +731,6 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr $loader->load('graphql/validator.xml'); } - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/validator.xml'); - } - $loader->load($config['use_symfony_listeners'] ? 'validator/events.xml' : 'validator/state.xml'); $container->registerForAutoconfiguration(ValidationGroupsGeneratorInterface::class) @@ -881,11 +746,6 @@ private function registerValidatorConfiguration(ContainerBuilder $container, arr $container->setParameter('api_platform.validator.serialize_payload_fields', $config['validator']['serialize_payload_fields']); $container->setParameter('api_platform.validator.query_parameter_validation', $config['validator']['query_parameter_validation']); - if (class_exists(QueryParameterValidator::class)) { - $loader->load('legacy/parameter_validator/parameter_validator.xml'); - $loader->load($config['use_symfony_listeners'] ? 'legacy/parameter_validator/events.xml' : 'legacy/parameter_validator/state.xml'); - } - if (!$config['validator']['query_parameter_validation']) { $container->removeDefinition('api_platform.listener.view.validate_query_parameters'); $container->removeDefinition('api_platform.validator.query_parameter_validator'); @@ -913,11 +773,6 @@ private function registerMercureConfiguration(ContainerBuilder $container, array } $container->setParameter('api_platform.mercure.include_type', $config['mercure']['include_type']); - - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/mercure.xml'); - } - $loader->load('state/mercure.xml'); if ($this->isConfigEnabled($container, $config['doctrine'])) { @@ -959,13 +814,6 @@ private function registerElasticsearchConfiguration(ContainerBuilder $container, ->addTag('api_platform.elasticsearch.request_body_search_extension.collection'); $container->setParameter('api_platform.elasticsearch.hosts', $config['elasticsearch']['hosts']); $loader->load('elasticsearch.xml'); - - // @phpstan-ignore-next-line - if (\Elasticsearch\Client::class === $clientClass) { - $loader->load('legacy/elasticsearch.xml'); - $container->setParameter('api_platform.elasticsearch.mapping', $config['elasticsearch']['mapping']); - $container->setDefinition('api_platform.elasticsearch.client_for_metadata', $clientDefinition); - } } private function registerSecurityConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void @@ -979,10 +827,6 @@ private function registerSecurityConfiguration(ContainerBuilder $container, arra $loader->load('security.xml'); - if ($config['event_listeners_backward_compatibility_layer']) { - $loader->load('legacy/security.xml'); - } - $loader->load('state/security.xml'); if (interface_exists(ValidatorInterface::class)) { @@ -1005,10 +849,6 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.openapi.overrideResponses', $config['openapi']['overrideResponses']); $loader->load('json_schema.xml'); - - if ($config['use_deprecated_json_schema_type_factory'] ?? false) { - $container->getDefinition('api_platform.json_schema.schema_factory')->setArgument('$typeFactory', new Reference('api_platform.json_schema.type_factory')); - } } private function registerMakerConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void @@ -1025,15 +865,6 @@ private function registerArgumentResolverConfiguration(XmlFileLoader $loader): v $loader->load('argument_resolver.xml'); } - private function registerInflectorConfiguration(ContainerBuilder $container, array $config): void - { - $container->setParameter('api_platform.keep_legacy_inflector', $config['keep_legacy_inflector'] ?? false); - - if ($config['keep_legacy_inflector']) { - trigger_deprecation('api-platform/core', '3.2', 'Using doctrine/inflector is deprecated since API Platform 3.2 and will be removed in API Platform 4. Use symfony/string instead. Run "composer require symfony/string" and set "keep_legacy_inflector" to false in config.'); - } - } - private function registerLinkSecurityConfiguration(XmlFileLoader $loader, array $config): void { if ($config['enable_link_security']) { diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php new file mode 100644 index 00000000000..7a9cc0d5c1d --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/SerializerMappingLoaderPass.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +final class SerializerMappingLoaderPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); + $loaders = $chainLoader->getArgument(0); + $loaders[] = $container->getDefinition('api_platform.serializer.property_metadata_loader'); + $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $loaders); + } +} diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index f37c42860d9..f31ab980e97 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -14,16 +14,12 @@ namespace ApiPlatform\Symfony\Bundle\DependencyInjection; use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; -use ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface; use ApiPlatform\Symfony\Controller\MainController; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; -use ApiPlatform\Validator\Exception\ValidationException; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; use Doctrine\ORM\EntityManagerInterface; @@ -41,6 +37,7 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Yaml\Yaml; /** * The configuration of the bundle. @@ -86,9 +83,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue('0.0.0') ->end() ->booleanNode('show_webby')->defaultTrue()->info('If true, show Webby on the documentation page')->end() - ->booleanNode('event_listeners_backward_compatibility_layer')->defaultNull()->info('If true API Platform uses Symfony event listeners instead of providers and processors.')->end() - ->booleanNode('use_deprecated_json_schema_type_factory')->defaultNull()->info('Use the deprecated type factory, this option will be removed in 4.0.')->end() - ->booleanNode('use_symfony_listeners')->defaultNull()->info(sprintf('Uses Symfony event listeners instead of the %s.', MainController::class))->end() + ->booleanNode('use_symfony_listeners')->defaultFalse()->info(sprintf('Uses Symfony event listeners instead of the %s.', MainController::class))->end() ->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end() ->scalarNode('asset_package')->defaultNull()->info('Specify an asset package name to use.')->end() ->scalarNode('path_segment_name_generator')->defaultValue('api_platform.metadata.path_segment_name_generator.underscore')->info('Specify a path name generator to use.')->end() @@ -98,7 +93,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->variableNode('serialize_payload_fields')->defaultValue([])->info('Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly.')->end() ->booleanNode('query_parameter_validation')->defaultValue(true)->end() - ->booleanNode('legacy_validation_exception')->defaultValue(true)->info('Uses the legacy "%s" instead of "%s".', LegacyValidationException::class, ValidationException::class)->end() ->end() ->end() ->arrayNode('eager_loading') @@ -117,7 +111,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('enable_entrypoint')->defaultTrue()->info('Enable the entrypoint')->end() ->booleanNode('enable_docs')->defaultTrue()->info('Enable the docs')->end() ->booleanNode('enable_profiler')->defaultTrue()->info('Enable the data collector and the WebProfilerBundle integration.')->end() - ->booleanNode('keep_legacy_inflector')->defaultTrue()->info('Keep doctrine/inflector instead of symfony/string to generate plurals for routes.')->end() ->booleanNode('enable_link_security')->defaultFalse()->info('Enable security for Links (sub resources)')->end() ->arrayNode('collection') ->addDefaultsIfNotSet() @@ -152,7 +145,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('serializer') ->addDefaultsIfNotSet() ->children() - ->booleanNode('hydra_prefix')->defaultNull()->info('Use the "hydra:" prefix.')->end() + ->booleanNode('hydra_prefix')->defaultFalse()->info('Use the "hydra:" prefix.')->end() ->end() ->end() ->end(); @@ -172,17 +165,24 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addExceptionToStatusSection($rootNode); $this->addFormatSection($rootNode, 'formats', [ + 'jsonld' => ['mime_types' => ['application/ld+json']] ]); $this->addFormatSection($rootNode, 'patch_formats', [ 'json' => ['mime_types' => ['application/merge-patch+json']], ]); - $this->addFormatSection($rootNode, 'docs_formats', [ - 'jsonopenapi' => ['mime_types' => ['application/vnd.openapi+json']], - 'yamlopenapi' => ['mime_types' => ['application/vnd.openapi+yaml']], - 'json' => ['mime_types' => ['application/json']], // this is only for legacy reasons, use jsonopenapi instead + + $defaultDocFormats = [ 'jsonld' => ['mime_types' => ['application/ld+json']], + 'jsonopenapi' => ['mime_types' => ['application/vnd.openapi+json']], 'html' => ['mime_types' => ['text/html']], - ]); + ]; + + if (class_exists(Yaml::class)) { + $defaultDocFormats['yamlopenapi'] = ['mime_types' => ['application/vnd.openapi+yaml']]; + } + + $this->addFormatSection($rootNode, 'docs_formats', $defaultDocFormats); + $this->addFormatSection($rootNode, 'error_formats', [ 'jsonld' => ['mime_types' => ['application/ld+json']], 'jsonproblem' => ['mime_types' => ['application/problem+json']], @@ -460,17 +460,6 @@ private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void ->defaultValue([]) ->prototype('scalar')->end() ->end() - ->arrayNode('mapping') - ->setDeprecated('api-platform/core', '3.1', sprintf('The "%%node%%" option is deprecated. Configure an %s as $stateOptions.', Options::class)) - ->normalizeKeys(false) - ->useAttributeAsKey('resource_class') - ->prototype('array') - ->children() - ->scalarNode('index')->defaultNull()->end() - ->scalarNode('type')->defaultValue(class_exists(DocumentMetadata::class) ? DocumentMetadata::DEFAULT_TYPE : '_doc')->end() - ->end() - ->end() - ->end() ->end() ->end() ->end(); @@ -524,7 +513,6 @@ private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode): voi ->defaultValue([ SerializerExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST, - ValidationExceptionInterface::class => Response::HTTP_BAD_REQUEST, OptimisticLockException::class => Response::HTTP_CONFLICT, ]) ->info('The list of exceptions mapped to their HTTP status code.') diff --git a/src/Symfony/Bundle/EventListener/SwaggerUiListener.php b/src/Symfony/Bundle/EventListener/SwaggerUiListener.php deleted file mode 100644 index 094d2ff6f23..00000000000 --- a/src/Symfony/Bundle/EventListener/SwaggerUiListener.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Bundle\EventListener; - -use Symfony\Component\HttpKernel\Event\RequestEvent; - -final class SwaggerUiListener -{ - /** - * Sets SwaggerUiAction as controller if the requested format is HTML. - */ - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - if ( - 'html' !== $request->getRequestFormat('') - || !($request->attributes->has('_api_resource_class') || $request->attributes->getBoolean('_api_respond', false)) - ) { - return; - } - - if (($operation = $request->attributes->get('_api_operation')) && 'api_platform.symfony.main_controller' === $operation->getController()) { - return; - } - - $request->attributes->set('_controller', 'api_platform.swagger_ui.action'); - } -} diff --git a/src/Symfony/Bundle/Resources/config/api.xml b/src/Symfony/Bundle/Resources/config/api.xml index 03a9ad00db8..7bce6a8f3a8 100644 --- a/src/Symfony/Bundle/Resources/config/api.xml +++ b/src/Symfony/Bundle/Resources/config/api.xml @@ -6,8 +6,8 @@ - + @@ -15,17 +15,11 @@ - - - - - - @@ -44,7 +38,6 @@ - @@ -103,17 +96,7 @@ - - %api_platform.keep_legacy_inflector% - - - - - - %api_platform.error_formats% - %api_platform.exception_to_status% - - + @@ -150,7 +133,6 @@ - @@ -179,7 +161,6 @@ - @@ -194,5 +175,9 @@ + + + + diff --git a/src/Symfony/Bundle/Resources/config/elasticsearch.xml b/src/Symfony/Bundle/Resources/config/elasticsearch.xml index d5fc1d0730f..04aac16ee22 100644 --- a/src/Symfony/Bundle/Resources/config/elasticsearch.xml +++ b/src/Symfony/Bundle/Resources/config/elasticsearch.xml @@ -74,7 +74,6 @@ - @@ -85,7 +84,6 @@ - @@ -97,10 +95,7 @@ - - false - diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index f532c6ab500..c58b39b1c23 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -119,11 +119,7 @@ - - - - - + @@ -188,20 +184,12 @@ - + - - - - - - %api_platform.graphql.nesting_separator% - - diff --git a/src/Symfony/Bundle/Resources/config/hydra.xml b/src/Symfony/Bundle/Resources/config/hydra.xml index b0525914328..021b885d3b9 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Symfony/Bundle/Resources/config/hydra.xml @@ -23,7 +23,6 @@ - %api_platform.validator.serialize_payload_fields% %api_platform.serializer.default_context% @@ -39,15 +38,6 @@ - - - - %kernel.debug% - %api_platform.serializer.default_context% - - - - @@ -79,6 +69,7 @@ + %api_platform.serializer.default_context% diff --git a/src/Symfony/Bundle/Resources/config/json_schema.xml b/src/Symfony/Bundle/Resources/config/json_schema.xml index d4a740601fc..aa2ba09cc50 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.xml +++ b/src/Symfony/Bundle/Resources/config/json_schema.xml @@ -7,16 +7,7 @@ - - - - - - - - - null diff --git a/src/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Symfony/Bundle/Resources/config/jsonapi.xml index 106ff577d01..40776f2fff7 100644 --- a/src/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Symfony/Bundle/Resources/config/jsonapi.xml @@ -74,12 +74,8 @@ - - %kernel.debug% - - - + diff --git a/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml b/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml deleted file mode 100644 index edff49165b8..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/elasticsearch.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - %api_platform.elasticsearch.mapping% - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/events.xml b/src/Symfony/Bundle/Resources/config/legacy/events.xml deleted file mode 100644 index 9169d043490..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/events.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - %api_platform.formats% - %api_platform.error_formats% - %api_platform.docs_formats% - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %api_platform.error_formats% - %api_platform.exception_to_status% - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/graphql.xml b/src/Symfony/Bundle/Resources/config/legacy/graphql.xml deleted file mode 100644 index 2045331f75a..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/graphql.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %api_platform.graphql.nesting_separator% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/http_cache.xml b/src/Symfony/Bundle/Resources/config/legacy/http_cache.xml deleted file mode 100644 index c09fc896711..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/http_cache.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - %api_platform.http_cache.etag% - %api_platform.http_cache.max_age% - %api_platform.http_cache.shared_max_age% - %api_platform.http_cache.vary% - %api_platform.http_cache.public% - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/http_cache_purger.xml b/src/Symfony/Bundle/Resources/config/legacy/http_cache_purger.xml deleted file mode 100644 index 7cddb16affa..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/http_cache_purger.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/hydra.xml b/src/Symfony/Bundle/Resources/config/legacy/hydra.xml deleted file mode 100644 index 5e7b592dc97..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/hydra.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/jsonapi.xml b/src/Symfony/Bundle/Resources/config/legacy/jsonapi.xml deleted file mode 100644 index c4803cf2ce4..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/jsonapi.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - %api_platform.collection.order_parameter_name% - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/mercure.xml b/src/Symfony/Bundle/Resources/config/legacy/mercure.xml deleted file mode 100644 index 5dde0e384bb..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/mercure.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/events.xml b/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/events.xml deleted file mode 100644 index c2cdaa128cf..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/events.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/parameter_validator.xml b/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/parameter_validator.xml deleted file mode 100644 index 89ea18cfabc..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/parameter_validator.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/state.xml b/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/state.xml deleted file mode 100644 index 7fd2a0249f6..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/parameter_validator/state.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/problem.xml b/src/Symfony/Bundle/Resources/config/legacy/problem.xml deleted file mode 100644 index fdd946f8ea6..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/problem.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - %api_platform.validator.serialize_payload_fields% - - - - - - - - %kernel.debug% - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/security.xml b/src/Symfony/Bundle/Resources/config/legacy/security.xml deleted file mode 100644 index e36a91a9dd4..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/security.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/swagger_ui.xml b/src/Symfony/Bundle/Resources/config/legacy/swagger_ui.xml deleted file mode 100644 index 20dc848a1d4..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/swagger_ui.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/legacy/validator.xml b/src/Symfony/Bundle/Resources/config/legacy/validator.xml deleted file mode 100644 index 59146689db7..00000000000 --- a/src/Symfony/Bundle/Resources/config/legacy/validator.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index c1181121e0d..59b9422a9df 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -52,6 +52,7 @@ + %api_platform.graphql.enabled% @@ -78,11 +79,9 @@ - - - - + + diff --git a/src/Symfony/Bundle/Resources/config/openapi.xml b/src/Symfony/Bundle/Resources/config/openapi.xml index 421cf2cf285..2f1a7391ac1 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Symfony/Bundle/Resources/config/openapi.xml @@ -62,7 +62,7 @@ - + @@ -85,7 +85,6 @@ - null %api_platform.formats% @@ -93,6 +92,10 @@ + + + + jsonopenapi diff --git a/src/Symfony/Bundle/Resources/config/state/provider.xml b/src/Symfony/Bundle/Resources/config/state/provider.xml index 828b8cacdf9..52bb2ea23f7 100644 --- a/src/Symfony/Bundle/Resources/config/state/provider.xml +++ b/src/Symfony/Bundle/Resources/config/state/provider.xml @@ -25,11 +25,6 @@ - - - - - api_platform.symfony.main_controller @@ -40,8 +35,7 @@ %api_platform.exception_to_status% null - null - %api_platform.rfc_7807_compliant_errors% + diff --git a/src/Symfony/Bundle/Resources/config/state/state.xml b/src/Symfony/Bundle/Resources/config/state/state.xml index 5e0e76358de..694ed28ddce 100644 --- a/src/Symfony/Bundle/Resources/config/state/state.xml +++ b/src/Symfony/Bundle/Resources/config/state/state.xml @@ -55,5 +55,10 @@ + + + + + diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.xml b/src/Symfony/Bundle/Resources/config/symfony/events.xml index 126ac746440..4ecdb892921 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/events.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/events.xml @@ -28,7 +28,6 @@ - null @@ -83,8 +82,8 @@ - + @@ -99,8 +98,7 @@ %api_platform.exception_to_status% null - null - %api_platform.rfc_7807_compliant_errors% + @@ -154,5 +152,13 @@ %api_platform.docs_formats% + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/symfony/symfony.xml b/src/Symfony/Bundle/Resources/config/symfony/symfony.xml index 897b158b804..f16daef8574 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/symfony.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/symfony.xml @@ -4,19 +4,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - - - - - - - - - %api_platform.handle_symfony_errors% diff --git a/src/Symfony/Bundle/Resources/config/validator/events.xml b/src/Symfony/Bundle/Resources/config/validator/events.xml index 2d2fcc172d8..449538eb37f 100644 --- a/src/Symfony/Bundle/Resources/config/validator/events.xml +++ b/src/Symfony/Bundle/Resources/config/validator/events.xml @@ -21,13 +21,6 @@ - - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/validator/validator.xml b/src/Symfony/Bundle/Resources/config/validator/validator.xml index 2c0afa30b7d..e22897eaf21 100644 --- a/src/Symfony/Bundle/Resources/config/validator/validator.xml +++ b/src/Symfony/Bundle/Resources/config/validator/validator.xml @@ -8,12 +8,17 @@ - %api_platform.validator.legacy_validation_exception% + + + + + + diff --git a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php index 05758146726..193cb77723c 100644 --- a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php +++ b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php @@ -80,7 +80,8 @@ public function process(mixed $openApi, Operation $operation, array $uriVariable $status = 200; $requestedOperation = $request?->attributes->get('_api_requested_operation') ?? null; if ($request->isMethodSafe() && $requestedOperation && $requestedOperation->getName()) { - $swaggerData['id'] = $request->get('id'); + // TODO: what if the parameter is named something else then `id`? + $swaggerData['id'] = ($request->attributes->get('_api_original_uri_variables') ?? [])['id'] ?? null; $swaggerData['queryParameters'] = $request->query->all(); $swaggerData['shortName'] = $requestedOperation->getShortName(); diff --git a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php index 3f11a10d91a..ac6372f0103 100644 --- a/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php +++ b/src/Symfony/Bundle/Test/ApiTestAssertionsTrait.php @@ -97,7 +97,6 @@ public static function assertJsonEquals(array|string $json, string $message = '' * @see https://github.com/sebastianbergmann/phpunit/issues/3494 * * @throws ExpectationFailedException - * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException * @throws \Exception */ public static function assertArraySubset(iterable $subset, iterable $array, bool $checkForObjectIdentity = false, string $message = ''): void diff --git a/src/Symfony/Bundle/Test/ApiTestCase.php b/src/Symfony/Bundle/Test/ApiTestCase.php index 9096d7151d2..3302b15a58f 100644 --- a/src/Symfony/Bundle/Test/ApiTestCase.php +++ b/src/Symfony/Bundle/Test/ApiTestCase.php @@ -28,16 +28,6 @@ abstract class ApiTestCase extends KernelTestCase { use ApiTestAssertionsTrait; - /** - * {@inheritdoc} - */ - protected function tearDown(): void - { - parent::tearDown(); - - self::getClient(null); - } - /** * Creates a Client. * diff --git a/src/Symfony/Controller/MainController.php b/src/Symfony/Controller/MainController.php index 6e937ea95ef..beafc6922cd 100644 --- a/src/Symfony/Controller/MainController.php +++ b/src/Symfony/Controller/MainController.php @@ -56,6 +56,7 @@ public function __invoke(Request $request): Response if (!$operation instanceof Error) { try { $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); + $request->attributes->set('_api_uri_variables', $uriVariables); } catch (InvalidIdentifierException|InvalidUriVariableException $e) { throw new NotFoundHttpException('Invalid uri variables.', $e); } diff --git a/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php b/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php index daa161caaa3..22c905a6ea3 100644 --- a/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php +++ b/src/Symfony/Doctrine/EventListener/PublishMercureUpdatesListener.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Symfony\Doctrine\EventListener; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Doctrine\Common\Messenger\DispatchTrait; use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface as GraphQlMercureSubscriptionIriGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface as GraphQlSubscriptionManagerInterface; @@ -67,7 +65,7 @@ final class PublishMercureUpdatesListener /** * @param array $formats */ - public function __construct(LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, private readonly LegacyIriConverterInterface|IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly SerializerInterface $serializer, private readonly array $formats, ?MessageBusInterface $messageBus = null, private readonly ?HubRegistry $hubRegistry = null, private readonly ?GraphQlSubscriptionManagerInterface $graphQlSubscriptionManager = null, private readonly ?GraphQlMercureSubscriptionIriGeneratorInterface $graphQlMercureSubscriptionIriGenerator = null, ?ExpressionLanguage $expressionLanguage = null, private bool $includeType = false) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly SerializerInterface $serializer, private readonly array $formats, ?MessageBusInterface $messageBus = null, private readonly ?HubRegistry $hubRegistry = null, private readonly ?GraphQlSubscriptionManagerInterface $graphQlSubscriptionManager = null, private readonly ?GraphQlMercureSubscriptionIriGeneratorInterface $graphQlMercureSubscriptionIriGenerator = null, ?ExpressionLanguage $expressionLanguage = null, private bool $includeType = false) { if (null === $messageBus && null === $hubRegistry) { throw new InvalidArgumentException('A message bus or a hub registry must be provided.'); diff --git a/src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php b/src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php index 34b9c67f532..8fdd0b3c964 100644 --- a/src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php +++ b/src/Symfony/Doctrine/EventListener/PurgeHttpCacheListener.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Symfony\Doctrine\EventListener; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\OperationNotFoundException; @@ -43,7 +41,7 @@ final class PurgeHttpCacheListener private readonly PropertyAccessorInterface $propertyAccessor; private array $tags = []; - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null) + public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } diff --git a/src/Symfony/EventListener/AddFormatListener.php b/src/Symfony/EventListener/AddFormatListener.php index 700c6599cec..efbe15dd3d0 100644 --- a/src/Symfony/EventListener/AddFormatListener.php +++ b/src/Symfony/EventListener/AddFormatListener.php @@ -13,16 +13,10 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\FormatMatcher; -use ApiPlatform\Metadata\Error as ErrorOperation; -use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Negotiation\Exception\InvalidArgument; -use Negotiation\Negotiator; -use Symfony\Component\HttpFoundation\Request; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -36,21 +30,13 @@ final class AddFormatListener { use OperationRequestInitiatorTrait; - private ?Negotiator $negotiator; - private ?ProviderInterface $provider = null; - /** - * @param ProviderInterface|Negotiator $negotiator + * @param ProviderInterface $provider */ - public function __construct($negotiator, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly array $formats = [], private readonly array $errorFormats = [], private readonly array $docsFormats = [], private readonly ?bool $eventsBackwardCompatibility = null) // @phpstan-ignore-line - { - if ($negotiator instanceof ProviderInterface) { - $this->provider = $negotiator; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, Negotiator::class); - $this->negotiator = $negotiator; - } - + public function __construct( + private ProviderInterface $provider, + ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, + ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } @@ -64,182 +50,15 @@ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $operation = $this->initializeOperation($request); - - // TODO: legacy code - if ($request->attributes->get('_api_exception_action')) { - return; - } - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) { - return; - } - - if ($operation && $this->provider) { - $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ - 'request' => $request, - 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], - 'resource_class' => $operation->getClass(), - ]); - - return; - } - - // TODO: the code below needs to be removed in 4.x - if ($this->provider && !$operation) { - return; - } - - if ('api_platform.action.entrypoint' === $request->attributes->get('_controller')) { - return; - } - - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - if ($operation instanceof ErrorOperation) { - return; - } - - if (!( - $request->attributes->has('_api_resource_class') - || $request->attributes->getBoolean('_api_respond', false) - || $request->attributes->getBoolean('_graphql', false) - )) { - return; - } - - $formats = $operation?->getOutputFormats() ?? ('api_doc' === $request->attributes->get('_route') ? $this->docsFormats : $this->formats); - - $this->addRequestFormats($request, $formats); - - // Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format) - if (null === $routeFormat = $request->attributes->get('_format') ?: null) { - $flattenedMimeTypes = $this->flattenMimeTypes($formats); - $mimeTypes = array_keys($flattenedMimeTypes); - } elseif (!isset($formats[$routeFormat])) { - if (!$request->attributes->get('data') instanceof \Exception) { - throw new NotFoundHttpException(\sprintf('Format "%s" is not supported', $routeFormat)); - } - $this->setRequestErrorFormat($operation, $request); - - return; - } else { - $mimeTypes = Request::getMimeTypes($routeFormat); - $flattenedMimeTypes = $this->flattenMimeTypes([$routeFormat => $mimeTypes]); - } - - // First, try to guess the format from the Accept header - /** @var string|null $accept */ - $accept = $request->headers->get('Accept'); - - if (null !== $accept) { - $mediaType = null; - try { - $mediaType = $this->negotiator->getBest($accept, $mimeTypes); - } catch (InvalidArgument) { - throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes); - } - - if (null === $mediaType) { - if (!$request->attributes->get('data') instanceof \Exception) { - throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes); - } - - $this->setRequestErrorFormat($operation, $request); - - return; - } - $formatMatcher = new FormatMatcher($formats); - $request->setRequestFormat($formatMatcher->getFormat($mediaType->getType())); - - return; - } - - // Then use the Symfony request format if available and applicable - $requestFormat = $request->getRequestFormat('') ?: null; - if (null !== $requestFormat) { - $mimeType = $request->getMimeType($requestFormat); - - if (isset($flattenedMimeTypes[$mimeType])) { - return; - } - - if ($request->attributes->get('data') instanceof \Exception) { - $this->setRequestErrorFormat($operation, $request); - - return; - } - - throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes); - } - - // Finally, if no Accept header nor Symfony request format is set, return the default format - foreach ($formats as $format => $mimeType) { - $request->setRequestFormat($format); - - return; - } - } - - /** - * Adds the supported formats to the request. - * - * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work. - */ - private function addRequestFormats(Request $request, array $formats): void - { - foreach ($formats as $format => $mimeTypes) { - $request->setFormat($format, (array) $mimeTypes); - } - } - - /** - * Retries the flattened list of MIME types. - */ - private function flattenMimeTypes(array $formats): array - { - $flattenedMimeTypes = []; - foreach ($formats as $format => $mimeTypes) { - foreach ($mimeTypes as $mimeType) { - $flattenedMimeTypes[$mimeType] = $format; - } - } - - return $flattenedMimeTypes; - } - - /** - * Retrieves an instance of NotAcceptableHttpException. - */ - private function getNotAcceptableHttpException(string $accept, array $mimeTypes): NotAcceptableHttpException - { - return new NotAcceptableHttpException(\sprintf( - 'Requested format "%s" is not supported. Supported MIME types are "%s".', - $accept, - implode('", "', array_keys($mimeTypes)) - )); - } - - public function setRequestErrorFormat(?HttpOperation $operation, Request $request): void - { - $errorResourceFormats = array_merge($operation?->getOutputFormats() ?? [], $operation?->getFormats() ?? [], $this->errorFormats); - - $flattened = $this->flattenMimeTypes($errorResourceFormats); - if ($flattened[$accept = $request->headers->get('Accept')] ?? false) { - $request->setRequestFormat($flattened[$accept]); - - return; - } - - if (isset($errorResourceFormats['jsonproblem'])) { - $request->setRequestFormat('jsonproblem'); - $request->setFormat('jsonproblem', $errorResourceFormats['jsonproblem']); - + if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond')) || !$operation) { return; } - $request->setRequestFormat(array_key_first($errorResourceFormats)); + $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ + 'request' => $request, + 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], + 'resource_class' => $operation->getClass(), + ]); } } diff --git a/src/Symfony/EventListener/AddHeadersListener.php b/src/Symfony/EventListener/AddHeadersListener.php deleted file mode 100644 index 739ea38a6c6..00000000000 --- a/src/Symfony/EventListener/AddHeadersListener.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Configures cache HTTP headers for the current response. - * - * @deprecated use ApiPlatform\HttpCache\State\AddHeadersProcessor instead - * - * @author Kévin Dunglas - */ -final class AddHeadersListener -{ - use OperationRequestInitiatorTrait; - - public function __construct(private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (!$request->isMethodCacheable() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (\count($attributes) < 1) { - return; - } - - $response = $event->getResponse(); - - if (!$response->getContent() || !$response->isSuccessful()) { - return; - } - - $operation = $this->initializeOperation($request); - $resourceCacheHeaders = $attributes['cache_headers'] ?? $operation?->getCacheHeaders() ?? []; - - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $response->getContent())); - } - - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } - - $vary = $resourceCacheHeaders['vary'] ?? $this->vary; - if (null !== $vary) { - $response->setVary(array_diff($vary, $response->getVary()), false); - } - - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - } -} diff --git a/src/Symfony/EventListener/AddLinkHeaderListener.php b/src/Symfony/EventListener/AddLinkHeaderListener.php deleted file mode 100644 index 49983c95343..00000000000 --- a/src/Symfony/EventListener/AddLinkHeaderListener.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\CorsTrait; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Psr\Link\LinkProviderInterface; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\Mercure\Discovery; -use Symfony\Component\WebLink\HttpHeaderSerializer; - -/** - * Adds the HTTP Link header pointing to the Mercure hub for resources having their updates dispatched. - * - * @deprecated use ApiPlatform\Symfony\State\MercureLinkProcessor instead - * - * @author Kévin Dunglas - */ -final class AddLinkHeaderListener -{ - use CorsTrait; - use OperationRequestInitiatorTrait; - - public function __construct( - private readonly Discovery $discovery, - ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, - private readonly HttpHeaderSerializer $serializer = new HttpHeaderSerializer(), - ) { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Sends the Mercure header on each response. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - - // API Platform 3.2 has a MainController where everything is handled by processors/providers - if ('api_platform.symfony.main_controller' === $operation?->getController() || $this->isPreflightRequest($request) || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - // Does the same as the web-link AddLinkHeaderListener as we want to use `_api_platform_links` not `_links`, - // note that the AddLinkHeaderProcessor is doing it with the MainController - $linkProvider = $event->getRequest()->attributes->get('_api_platform_links'); - if ($operation && $linkProvider instanceof LinkProviderInterface && $links = $linkProvider->getLinks()) { - $event->getResponse()->headers->set('Link', $this->serializer->serialize($links), false); - } - - if ( - null === $request->attributes->get('_api_resource_class') - || !($attributes = RequestAttributesExtractor::extractAttributes($request)) - ) { - return; - } - - $mercure = $operation?->getMercure() ?? ($attributes['mercure'] ?? false); - - if (!$mercure) { - return; - } - - $hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null; - $this->discovery->addLink($request, $hub); - } -} diff --git a/src/Symfony/EventListener/AddTagsListener.php b/src/Symfony/EventListener/AddTagsListener.php deleted file mode 100644 index e541dd7586e..00000000000 --- a/src/Symfony/EventListener/AddTagsListener.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\EventListener; - -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Sets the list of resources' IRIs included in this response in the configured cache tag HTTP header and/or "xkey" HTTP headers. - * - * By default the "Cache-Tags" HTTP header is used because it is supported by CloudFlare. - * - * @see https://developers.cloudflare.com/cache/how-to/purge-cache#add-cache-tag-http-response-headers - * - * The "xkey" is used because it is supported by Varnish. - * @see https://docs.varnish-software.com/varnish-cache-plus/vmods/ykey/ - * @deprecated use ApiPlatform\HttpCache\State\AddTagsProcessor instead - * - * @author Kévin Dunglas - */ -final class AddTagsListener -{ - use OperationRequestInitiatorTrait; - use UriVariablesResolverTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?PurgerInterface $purger = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Adds the configured HTTP cache tag and "xkey" headers. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - $response = $event->getResponse(); - - if ( - !$request->isMethodCacheable() - || !$response->isCacheable() - || (!$attributes = RequestAttributesExtractor::extractAttributes($request)) - || $request->attributes->get('_api_platform_disable_listeners') - ) { - return; - } - - $resources = $request->attributes->get('_resources'); - if ($operation instanceof CollectionOperationInterface) { - // Allows to purge collections - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); - $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); - - $resources[$iri] = $iri; - } - - if (!$resources) { - return; - } - - if (!$this->purger) { - $response->headers->set('Cache-Tags', implode(',', $resources)); - - return; - } - - $headers = $this->purger->getResponseHeaders($resources); - - foreach ($headers as $key => $value) { - $response->headers->set($key, $value); - } - } -} diff --git a/src/Symfony/EventListener/DenyAccessListener.php b/src/Symfony/EventListener/DenyAccessListener.php deleted file mode 100644 index bb7cad2f65b..00000000000 --- a/src/Symfony/EventListener/DenyAccessListener.php +++ /dev/null @@ -1,107 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\Event\ViewEvent; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; - -/** - * Denies access to the current resource if the logged user doesn't have sufficient permissions. - * - * @deprecated use ApiPlatform\Symfony\Security\State\AccessCheckerProvider instead - * - * @author Kévin Dunglas - */ -final class DenyAccessListener -{ - use OperationRequestInitiatorTrait; - - public function __construct(?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?ResourceAccessCheckerInterface $resourceAccessChecker = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onSecurity(RequestEvent $event): void - { - $this->checkSecurity($event->getRequest(), 'security'); - } - - public function onSecurityPostDenormalize(RequestEvent $event): void - { - $request = $event->getRequest(); - $this->checkSecurity($request, 'security_post_denormalize', [ - 'previous_object' => $request->attributes->get('previous_data'), - ]); - } - - public function onSecurityPostValidation(ViewEvent $event): void - { - $request = $event->getRequest(); - $this->checkSecurity($request, 'security_post_validation', [ - 'previous_object' => $request->attributes->get('previous_data'), - ]); - } - - /** - * @throws AccessDeniedException - */ - private function checkSecurity(Request $request, string $attribute, array $extraVariables = []): void - { - if ($request->attributes->get('_api_platform_disable_listeners') || !$this->resourceAccessChecker || !$attributes = RequestAttributesExtractor::extractAttributes($request)) { - return; - } - - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - - if (!$operation) { - return; - } - - switch ($attribute) { - case 'security_post_denormalize': - $isGranted = $operation->getSecurityPostDenormalize(); - $message = $operation->getSecurityPostDenormalizeMessage(); - break; - case 'security_post_validation': - $isGranted = $operation->getSecurityPostValidation(); - $message = $operation->getSecurityPostValidationMessage(); - break; - default: - $isGranted = $operation->getSecurity(); - $message = $operation->getSecurityMessage(); - } - - if (null === $isGranted) { - return; - } - - $extraVariables += $request->attributes->all(); - $extraVariables['object'] = $request->attributes->get('data'); - $extraVariables['previous_object'] = $request->attributes->get('previous_data'); - $extraVariables['request'] = $request; - - if (!$this->resourceAccessChecker->isGranted($attributes['resource_class'], $isGranted, $extraVariables)) { - throw new AccessDeniedException($message ?? 'Access Denied.'); - } - } -} diff --git a/src/Symfony/EventListener/DeserializeListener.php b/src/Symfony/EventListener/DeserializeListener.php index d60e4891c87..433f86149dc 100644 --- a/src/Symfony/EventListener/DeserializeListener.php +++ b/src/Symfony/EventListener/DeserializeListener.php @@ -13,28 +13,13 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\FormatMatcher; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; use ApiPlatform\State\ProviderInterface; -use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; -use Symfony\Component\HttpFoundation\Request; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; -use Symfony\Component\Serializer\Exception\PartialDenormalizationException; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\Constraints\Type; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; -use Symfony\Contracts\Translation\LocaleAwareInterface; -use Symfony\Contracts\Translation\TranslatorInterface; -use Symfony\Contracts\Translation\TranslatorTrait; /** * Updates the entity retrieved by the data provider with data contained in the request body. @@ -46,35 +31,15 @@ final class DeserializeListener use OperationRequestInitiatorTrait; public const OPERATION_ATTRIBUTE_KEY = 'deserialize'; - private SerializerInterface $serializer; - private ?ProviderInterface $provider = null; + /** + * @param ProviderInterface $provider + */ public function __construct( - ProviderInterface|SerializerInterface $serializer, - private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface|null $serializerContextBuilder = null, + private ProviderInterface $provider, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, - private ?TranslatorInterface $translator = null, ) { - if ($serializer instanceof ProviderInterface) { - $this->provider = $serializer; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, SerializerInterface::class); - $this->serializer = $serializer; - } - - if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) { - $resourceMetadataFactory = $serializerContextBuilder; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class); - } - $this->resourceMetadataCollectionFactory = $resourceMetadataFactory; - if (null === $this->translator) { - $this->translator = new class implements TranslatorInterface, LocaleAwareInterface { - use TranslatorTrait; - }; - $this->translator->setLocale('en'); - } } /** @@ -86,117 +51,30 @@ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $method = $request->getMethod(); + $operation = $this->initializeOperation($request); if ( !($attributes = RequestAttributesExtractor::extractAttributes($request)) || !$attributes['receive'] + || !$operation ) { return; } - $operation = $this->initializeOperation($request); - - if ($operation && $this->provider) { - if (null === $operation->canDeserialize() && $operation instanceof HttpOperation) { - $operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true)); - } - - if (!$operation->canDeserialize()) { - return; - } - - $data = $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ - 'request' => $request, - 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], - 'resource_class' => $operation->getClass(), - ]); - - $request->attributes->set('data', $data); - - return; - } - - // TODO: the code below needs to be removed in 4.x - if ( - 'DELETE' === $method - || $request->isMethodSafe() - || $request->attributes->get('_api_platform_disable_listeners') - ) { - return; + if (null === $operation->canDeserialize() && $operation instanceof HttpOperation) { + $operation = $operation->withDeserialize(\in_array($method, ['POST', 'PUT', 'PATCH'], true)); } - if ('api_platform.symfony.main_controller' === $operation?->getController()) { + if (!$operation->canDeserialize()) { return; } - if (!($operation?->canDeserialize() ?? true)) { - return; - } - - $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes); - - $format = $this->getFormat($request, $operation?->getInputFormats() ?? []); - $data = $request->attributes->get('data'); - if ( - null !== $data - && ( - 'POST' === $method - || 'PATCH' === $method - || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? false)) - ) - ) { - $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $data; - } - try { - $request->attributes->set( - 'data', - $this->serializer->deserialize($request->getContent(), $context['resource_class'], $format, $context) - ); - } catch (PartialDenormalizationException $e) { - $violations = new ConstraintViolationList(); - foreach ($e->getErrors() as $exception) { - if (!$exception instanceof NotNormalizableValueException) { - continue; - } - $message = (new Type($exception->getExpectedTypes() ?? []))->message; - $parameters = []; - if ($exception->canUseMessageForUser()) { - $parameters['hint'] = $exception->getMessage(); - } - $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, Type::INVALID_TYPE_ERROR)); - } - if (0 !== \count($violations)) { - throw new ValidationException($violations); - } - } - } - - /** - * Extracts the format from the Content-Type header and check that it is supported. - * - * @throws UnsupportedMediaTypeHttpException - */ - private function getFormat(Request $request, array $formats): string - { - /** @var ?string $contentType */ - $contentType = $request->headers->get('CONTENT_TYPE'); - if (null === $contentType || '' === $contentType) { - throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.'); - } - - $formatMatcher = new FormatMatcher($formats); - $format = $formatMatcher->getFormat($contentType); - if (null === $format) { - $supportedMimeTypes = []; - foreach ($formats as $mimeTypes) { - foreach ($mimeTypes as $mimeType) { - $supportedMimeTypes[] = $mimeType; - } - } - - throw new UnsupportedMediaTypeHttpException(\sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes))); - } + $data = $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ + 'request' => $request, + 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], + 'resource_class' => $operation->getClass(), + ]); - return $format; + $request->attributes->set('data', $data); } } diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php index cc37939e898..21dca6001a5 100644 --- a/src/Symfony/EventListener/ErrorListener.php +++ b/src/Symfony/EventListener/ErrorListener.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Error as ErrorOperation; use ApiPlatform\Metadata\Exception\HttpExceptionInterface; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; @@ -27,7 +25,6 @@ use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\State\Util\RequestAttributesExtractor; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use Negotiation\Negotiator; use Psr\Log\LoggerInterface; @@ -58,10 +55,9 @@ public function __construct( private readonly array $errorFormats = [], private readonly array $exceptionToStatus = [], /** @phpstan-ignore-next-line we're not using this anymore but keeping for bc layer */ - private readonly IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null, - private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface|null $resourceClassResolver = null, + private readonly ?IdentifiersExtractorInterface $identifiersExtractor = null, + private readonly ?ResourceClassResolverInterface $resourceClassResolver = null, ?Negotiator $negotiator = null, - private readonly ?bool $problemCompliantErrors = true, ) { parent::__construct($controller, $logger, $debug, $exceptionsMapping); $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; @@ -87,18 +83,6 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re return parent::duplicateRequest($exception, $request); } - $legacy = $apiOperation ? ($apiOperation->getExtraProperties()['rfc_7807_compliant_errors'] ?? false) : $this->problemCompliantErrors; - - if (!$this->problemCompliantErrors || !$legacy) { - trigger_deprecation('api-platform/core', '3.4', "rfc_7807_compliant_errors flag will be removed in 4.0, to handle errors yourself use extraProperties: ['rfc_7807_compliant_errors' => false]"); - $this->controller = 'api_platform.action.exception'; - $dup = parent::duplicateRequest($exception, $request); - $dup->attributes->set('_api_operation', $apiOperation); - $dup->attributes->set('_api_exception_action', true); - - return $dup; - } - if ($this->debug) { $this->logger?->error('An exception occured, transforming to an Error resource.', ['exception' => $exception, 'operation' => $apiOperation]); } @@ -129,6 +113,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re // These are for swagger $dup->attributes->set('_api_original_route', $request->attributes->get('_route')); $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params')); + $dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables')); $dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation')); $dup->attributes->set('_api_platform_disable_listeners', true); @@ -193,7 +178,7 @@ private function getStatusCode(?HttpOperation $apiOperation, Request $request, ? return 400; } - if ($exception instanceof ConstraintViolationListAwareExceptionInterface || $exception instanceof LegacyConstraintViolationListAwareExceptionInterface) { + if ($exception instanceof ConstraintViolationListAwareExceptionInterface) { return 422; } @@ -257,7 +242,7 @@ class: Error::class, // Create a generic, rfc7807 compatible error according to the wanted format $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); - // status code may be overriden by the exceptionToStatus option + // status code may be overridden by the exceptionToStatus option $statusCode = 500; if ($operation instanceof HttpOperation) { $statusCode = $this->getStatusCode($apiOperation, $request, $operation, $exception); diff --git a/src/Symfony/EventListener/QueryParameterValidateListener.php b/src/Symfony/EventListener/QueryParameterValidateListener.php deleted file mode 100644 index 6f799b07a1e..00000000000 --- a/src/Symfony/EventListener/QueryParameterValidateListener.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\EventListener; - -use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; -use ApiPlatform\Doctrine\Orm\State\Options; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\ParameterValidator\ParameterValidator; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestParser; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * Validates query parameters depending on filter description. - * - * @author Julien Deniau - * - * @deprecated - */ -final class QueryParameterValidateListener -{ - use OperationRequestInitiatorTrait; - - public const OPERATION_ATTRIBUTE_KEY = 'query_parameter_validate'; - private ?ParameterValidator $queryParameterValidator = null; - private ?ProviderInterface $provider = null; - - public function __construct($queryParameterValidator, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) - { - if ($queryParameterValidator instanceof ProviderInterface) { - $this->provider = $queryParameterValidator; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, ParameterValidator::class); - $this->queryParameterValidator = $queryParameterValidator; - } - - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - - if ( - !$request->isMethodSafe() - || !($attributes = RequestAttributesExtractor::extractAttributes($request)) - || 'GET' !== $request->getMethod() - || $request->attributes->get('_api_platform_disable_listeners') - ) { - return; - } - - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - - if (!($operation->getExtraProperties()['use_legacy_parameter_validator'] ?? true)) { - return; - } - - if (!($operation?->getQueryParameterValidationEnabled() ?? true) || !$operation instanceof HttpOperation) { - return; - } - - if ($this->provider instanceof ProviderInterface) { - if (null === $operation->getQueryParameterValidationEnabled()) { - $operation = $operation->withQueryParameterValidationEnabled('GET' === $request->getMethod()); - } - - $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ - 'request' => $request, - 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], - 'resource_class' => $operation->getClass(), - ]); - - return; - } - - $queryString = RequestParser::getQueryString($request); - $queryParameters = $queryString ? RequestParser::parseRequestParams($queryString) : []; - - $class = $attributes['resource_class']; - - if ($options = $operation->getStateOptions()) { - if ($options instanceof Options && $options->getEntityClass()) { - $class = $options->getEntityClass(); - } - - if ($options instanceof ODMOptions && $options->getDocumentClass()) { - $class = $options->getDocumentClass(); - } - } - - $this->queryParameterValidator->validateFilters($class, $operation->getFilters() ?? [], $queryParameters); - } -} diff --git a/src/Symfony/EventListener/ReadListener.php b/src/Symfony/EventListener/ReadListener.php index fae461308e3..475c2da59d1 100644 --- a/src/Symfony/EventListener/ReadListener.php +++ b/src/Symfony/EventListener/ReadListener.php @@ -13,25 +13,17 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Metadata\Error; use ApiPlatform\Metadata\Exception\InvalidIdentifierException; use ApiPlatform\Metadata\Exception\InvalidUriVariableException; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UriVariablesConverterInterface; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface; -use ApiPlatform\State\CallableProvider; -use ApiPlatform\State\Exception\ProviderNotFoundException; -use ApiPlatform\State\Provider\ReadProvider; use ApiPlatform\State\ProviderInterface; -use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestParser; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -46,18 +38,16 @@ final class ReadListener use OperationRequestInitiatorTrait; use UriVariablesResolverTrait; + /** + * @param ProviderInterface $provider + */ public function __construct( private readonly ProviderInterface $provider, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, - private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|null $serializerContextBuilder = null, - LegacyUriVariablesConverterInterface|UriVariablesConverterInterface|null $uriVariablesConverter = null, + ?UriVariablesConverterInterface $uriVariablesConverter = null, ) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; $this->uriVariablesConverter = $uriVariablesConverter; - - if ($provider instanceof CallableProvider) { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ReadProvider::class, self::class, $provider::class); - } } /** @@ -74,81 +64,30 @@ public function onKernelRequest(RequestEvent $event): void } $operation = $this->initializeOperation($request); - - if ($operation && !$this->provider instanceof CallableProvider) { - if (null === $operation->canRead()) { - $operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe()); - } - - $uriVariables = []; - if (!$operation instanceof Error && $operation instanceof HttpOperation) { - try { - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); - } catch (InvalidIdentifierException|InvalidUriVariableException $e) { - if ($operation->canRead()) { - throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); - } - } - } - - $request->attributes->set('_api_uri_variables', $uriVariables); - $this->provider->provide($operation, $uriVariables, [ - 'request' => $request, - 'uri_variables' => $uriVariables, - 'resource_class' => $operation->getClass(), - ]); - - return; - } - - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - if (!$operation || !($operation->canRead() ?? true) || (!$operation->getUriVariables() && !$request->isMethodSafe())) { + if (!$operation) { return; } - $context = ['operation' => $operation]; - - if (null === $filters = $request->attributes->get('_api_filters')) { - $queryString = RequestParser::getQueryString($request); - $filters = $queryString ? RequestParser::parseRequestParams($queryString) : null; - } - - if ($filters) { - $context['filters'] = $filters; + if (null === $operation->canRead()) { + $operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe()); } - if ($this->serializerContextBuilder) { - // Builtin data providers are able to use the serialization context to automatically add join clauses - $context += $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes); - $request->attributes->set('_api_normalization_context', $normalizationContext); - } - - $parameters = $request->attributes->all(); - $resourceClass = $operation->getClass() ?? $attributes['resource_class']; - try { - $uriVariables = $this->getOperationUriVariables($operation, $parameters, $resourceClass); - $data = $this->provider->provide($operation, $uriVariables, $context); - } catch (InvalidIdentifierException|InvalidUriVariableException $e) { - throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); - } catch (ProviderNotFoundException $e) { - $data = null; - } - - if ( - null === $data - && 'POST' !== $operation->getMethod() - && ( - 'PUT' !== $operation->getMethod() - || ($operation instanceof Put && !($operation->getAllowCreate() ?? false)) - ) - ) { - throw new NotFoundHttpException('Not Found'); + $uriVariables = []; + if (!$operation instanceof Error && $operation instanceof HttpOperation) { + try { + $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); + } catch (InvalidIdentifierException|InvalidUriVariableException $e) { + if ($operation->canRead()) { + throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); + } + } } - $request->attributes->set('data', $data); - $request->attributes->set('previous_data', $this->clone($data)); + $request->attributes->set('_api_uri_variables', $uriVariables); + $this->provider->provide($operation, $uriVariables, [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + ]); } } diff --git a/src/Symfony/EventListener/RespondListener.php b/src/Symfony/EventListener/RespondListener.php index 2f9a600e44d..15c12c7f103 100644 --- a/src/Symfony/EventListener/RespondListener.php +++ b/src/Symfony/EventListener/RespondListener.php @@ -13,15 +13,10 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Metadata\Exception\HttpExceptionInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ViewEvent; @@ -34,23 +29,11 @@ final class RespondListener { use OperationRequestInitiatorTrait; - public const METHOD_TO_CODE = [ - 'POST' => Response::HTTP_CREATED, - 'DELETE' => Response::HTTP_NO_CONTENT, - ]; - - private IriConverterInterface|LegacyIriConverterInterface|null $iriConverter = null; - private ?ProcessorInterface $processor = null; - - public function __construct(?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, IriConverterInterface|LegacyIriConverterInterface|ProcessorInterface|null $iriConverter = null) + /** + * @param ProcessorInterface $processor + */ + public function __construct(private readonly ProcessorInterface $processor, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null) { - if ($iriConverter instanceof ProcessorInterface) { - $this->processor = $iriConverter; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ProcessorInterface::class, self::class, IriConverterInterface::class); - $this->iriConverter = $iriConverter; - } - $this->resourceMetadataCollectionFactory = $resourceMetadataFactory; } @@ -60,93 +43,21 @@ public function __construct(?ResourceMetadataCollectionFactoryInterface $resourc public function onKernelView(ViewEvent $event): void { $request = $event->getRequest(); - $controllerResult = $event->getControllerResult(); $operation = $this->initializeOperation($request); $attributes = RequestAttributesExtractor::extractAttributes($request); - if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) { - return; - } - - if ($operation && $this->processor instanceof ProcessorInterface) { - $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; - $response = $this->processor->process($controllerResult, $operation, $uriVariables, [ - 'request' => $request, - 'uri_variables' => $uriVariables, - 'resource_class' => $operation->getClass(), - 'original_data' => $request->attributes->get('original_data'), - ]); - - $event->setResponse($response); - - return; - } - - // TODO: the code below needs to be removed in 4.x - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - - if ($controllerResult instanceof Response && ($attributes['respond'] ?? false)) { - $event->setResponse($controllerResult); - - return; - } - - if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) { + if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond')) || !$operation) { return; } - $headers = [ - 'Content-Type' => \sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())), - 'Vary' => 'Accept', - 'X-Content-Type-Options' => 'nosniff', - 'X-Frame-Options' => 'deny', - ]; - - $status = $operation?->getStatus(); - - if ($sunset = $operation?->getSunset()) { - $headers['Sunset'] = (new \DateTimeImmutable($sunset))->format(\DateTime::RFC1123); - } - - if ($acceptPatch = $operation?->getAcceptPatch()) { - $headers['Accept-Patch'] = $acceptPatch; - } - - $method = $request->getMethod(); - if ( - $this->iriConverter - && $operation - && ($operation->getExtraProperties()['is_alternate_resource_metadata'] ?? false) - && 301 === $operation->getStatus() - ) { - $status = 301; - $headers['Location'] = $this->iriConverter->getIriFromResource($request->attributes->get('data'), UrlGeneratorInterface::ABS_PATH, $operation); - } elseif ('PUT' === $method && !($attributes['previous_data'] ?? null) && null === $status && ($operation instanceof Put && ($operation->getAllowCreate() ?? false))) { - $status = Response::HTTP_CREATED; - } - - $status ??= self::METHOD_TO_CODE[$request->getMethod()] ?? Response::HTTP_OK; - - if ($request->attributes->has('_api_write_item_iri')) { - $headers['Content-Location'] = $request->attributes->get('_api_write_item_iri'); - - if ((Response::HTTP_CREATED === $status || (300 <= $status && $status < 400)) && 'POST' === $method) { - $headers['Location'] = $request->attributes->get('_api_write_item_iri'); - } - } - - if (($exception = $request->attributes->get('data')) instanceof HttpExceptionInterface) { - $headers = array_merge($headers, $exception->getHeaders()); - } + $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; + $response = $this->processor->process($event->getControllerResult(), $operation, $uriVariables, [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + 'original_data' => $request->attributes->get('original_data'), + ]); - $event->setResponse(new Response( - $controllerResult, - $status, - $headers - )); + $event->setResponse($response); } } diff --git a/src/Symfony/EventListener/SerializeListener.php b/src/Symfony/EventListener/SerializeListener.php index c080c8c4538..16dfd17311b 100644 --- a/src/Symfony/EventListener/SerializeListener.php +++ b/src/Symfony/EventListener/SerializeListener.php @@ -13,26 +13,12 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; -use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\Error; -use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\ResourceList; -use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use ApiPlatform\Util\ErrorFormatGuesser; -use ApiPlatform\Validator\Exception\ValidationException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\ViewEvent; -use Symfony\Component\Serializer\Encoder\EncoderInterface; -use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\WebLink\GenericLinkProvider; -use Symfony\Component\WebLink\Link; /** * Serializes data. @@ -43,33 +29,11 @@ final class SerializeListener { use OperationRequestInitiatorTrait; - public const OPERATION_ATTRIBUTE_KEY = 'serialize'; - private ?SerializerInterface $serializer = null; - private ?ProcessorInterface $processor = null; - private ?SerializerContextBuilderInterface $serializerContextBuilder = null; - - public function __construct( - SerializerInterface|ProcessorInterface $serializer, - SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface|null $serializerContextBuilder = null, - ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, - private readonly array $errorFormats = [], - // @phpstan-ignore-next-line we don't need this anymore - private readonly bool $debug = false, - ) { - if ($serializer instanceof ProcessorInterface) { - $this->processor = $serializer; - } else { - $this->serializer = $serializer; - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProcessorInterface::class, self::class, SerializerInterface::class); - } - - if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) { - $resourceMetadataFactory = $serializerContextBuilder; - } else { - $this->serializerContextBuilder = $serializerContextBuilder; - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class); - } - + /** + * @param ProcessorInterface $processor + */ + public function __construct(private readonly ProcessorInterface $processor, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null) + { $this->resourceMetadataCollectionFactory = $resourceMetadataFactory; } @@ -84,122 +48,26 @@ public function onKernelView(ViewEvent $event): void $attributes = RequestAttributesExtractor::extractAttributes($request); - if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) { - return; - } - - if ($operation && $this->processor instanceof ProcessorInterface) { - if (null === $operation->canSerialize()) { - $operation = $operation->withSerialize(true); - } - - if ($operation instanceof Error) { - // we don't want the FlattenException - $controllerResult = $request->attributes->get('data') ?? $controllerResult; - } - - $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; - $serialized = $this->processor->process($controllerResult, $operation, $uriVariables, [ - 'request' => $request, - 'uri_variables' => $uriVariables, - 'resource_class' => $operation->getClass(), - ]); - - $event->setControllerResult($serialized); - - return; - } - - // TODO: the code below needs to be removed in 4.x - if ($controllerResult instanceof Response) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - - if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) { - return; - } - - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - if (!($operation?->canSerialize() ?? true)) { - return; - } - - if (!$attributes) { - $this->serializeRawData($event, $request, $controllerResult); - - return; - } - - $context = $this->serializerContextBuilder->createFromRequest($request, true, $attributes); - if (isset($context['output']) && \array_key_exists('class', $context['output']) && null === $context['output']['class']) { - $event->setControllerResult(null); - + if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false)) || !$operation) { return; } - if ($controllerResult instanceof ValidationException && class_exists(ErrorFormatGuesser::class)) { - $format = ErrorFormatGuesser::guessErrorFormat($request, $this->errorFormats); - $previousOperation = $request->attributes->get('_api_previous_operation'); - if (!($previousOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) { - $context['groups'] = ['legacy_'.$format['key']]; - $context['force_iri_generation'] = false; - } + if (null === $operation->canSerialize()) { + $operation = $operation->withSerialize(true); } - if ($included = $request->attributes->get('_api_included')) { - $context['api_included'] = $included; + if ($operation instanceof Error) { + // we don't want the FlattenException + $controllerResult = $request->attributes->get('data') ?? $controllerResult; } - $resources = new ResourceList(); - $context['resources'] = &$resources; - $context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'resources'; - $resourcesToPush = new ResourceList(); - $context['resources_to_push'] = &$resourcesToPush; - $context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'resources_to_push'; - if (($options = $operation?->getStateOptions()) && ( - ($options instanceof Options && $options->getEntityClass()) - || ($options instanceof ODMOptions && $options->getDocumentClass()) - )) { - $context['force_resource_class'] = $operation->getClass(); - } - - $request->attributes->set('_api_normalization_context', $context); - $event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $context)); - - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); - if (!\count($resourcesToPush)) { - return; - } - - $linkProvider = $request->attributes->get('_api_platform_links', new GenericLinkProvider()); - foreach ($resourcesToPush as $resourceToPush) { - $linkProvider = $linkProvider->withLink((new Link('preload', $resourceToPush))->withAttribute('as', 'fetch')); - } - $request->attributes->set('_api_platform_links', $linkProvider); - } - - /** - * Tries to serialize data that are not API resources (e.g. the entrypoint or data returned by a custom controller). - * - * @throws RuntimeException - */ - private function serializeRawData(ViewEvent $event, Request $request, $controllerResult): void - { - if (\is_object($controllerResult)) { - $event->setControllerResult($this->serializer->serialize($controllerResult, $request->getRequestFormat(), $request->attributes->get('_api_normalization_context', []))); - - return; - } - - if (!$this->serializer instanceof EncoderInterface) { - throw new RuntimeException(\sprintf('The serializer must implement the "%s" interface.', EncoderInterface::class)); - } + $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; + $serialized = $this->processor->process($controllerResult, $operation, $uriVariables, [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + ]); - $event->setControllerResult($this->serializer->encode($controllerResult, $request->getRequestFormat())); + $event->setControllerResult($serialized); } } diff --git a/src/Symfony/EventListener/ValidateListener.php b/src/Symfony/EventListener/ValidateListener.php index d3f3110e8ae..cd9d8eb9e39 100644 --- a/src/Symfony/EventListener/ValidateListener.php +++ b/src/Symfony/EventListener/ValidateListener.php @@ -17,8 +17,6 @@ use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\Validator\Exception\ValidationException; -use ApiPlatform\Validator\ValidatorInterface; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ViewEvent; /** @@ -30,20 +28,11 @@ final class ValidateListener { use OperationRequestInitiatorTrait; - public const OPERATION_ATTRIBUTE_KEY = 'validate'; - - private ValidatorInterface $validator; - private ?ProviderInterface $provider = null; - - public function __construct(ProviderInterface|ValidatorInterface $validator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) + /** + * @param ProviderInterface $provider + */ + public function __construct(private readonly ProviderInterface $provider, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) { - if ($validator instanceof ProviderInterface) { - $this->provider = $validator; - } else { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, ValidatorInterface::class); - $this->validator = $validator; - } - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } @@ -54,40 +43,21 @@ public function __construct(ProviderInterface|ValidatorInterface $validator, Res */ public function onKernelView(ViewEvent $event): void { - $controllerResult = $event->getControllerResult(); $request = $event->getRequest(); $operation = $this->initializeOperation($request); - if ($operation && $this->provider instanceof ProviderInterface) { - if (null === $operation->canValidate()) { - $operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE')); - } - - $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ - 'request' => $request, - 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], - 'resource_class' => $operation->getClass(), - ]); - + if (!$operation) { return; } - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { - return; - } - - if ( - $controllerResult instanceof Response - || $request->isMethodSafe() - || $request->isMethod('DELETE') - ) { - return; - } - - if (!$operation || !($operation->canValidate() ?? true)) { - return; + if (null === $operation->canValidate()) { + $operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE')); } - $this->validator->validate($controllerResult, $operation->getValidationContext() ?? []); + $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [ + 'request' => $request, + 'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [], + 'resource_class' => $operation->getClass(), + ]); } } diff --git a/src/Symfony/EventListener/WriteListener.php b/src/Symfony/EventListener/WriteListener.php index d5a79413c7e..1ee78b53d48 100644 --- a/src/Symfony/EventListener/WriteListener.php +++ b/src/Symfony/EventListener/WriteListener.php @@ -13,26 +13,18 @@ namespace ApiPlatform\Symfony\EventListener; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Metadata\Error; use ApiPlatform\Metadata\Exception\InvalidIdentifierException; use ApiPlatform\Metadata\Exception\InvalidUriVariableException; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UriVariablesConverterInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\State\CallableProcessor; -use ApiPlatform\State\Processor\WriteProcessor; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use Symfony\Component\HttpFoundation\Response; +use ApiPlatform\State\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -49,31 +41,15 @@ final class WriteListener use OperationRequestInitiatorTrait; use UriVariablesResolverTrait; - private LegacyIriConverterInterface|IriConverterInterface|null $iriConverter = null; - /** * @param ProcessorInterface $processor */ public function __construct( private readonly ProcessorInterface $processor, - LegacyIriConverterInterface|IriConverterInterface|ResourceMetadataCollectionFactoryInterface|null $iriConverter = null, - private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface|null $resourceClassResolver = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, - LegacyUriVariablesConverterInterface|UriVariablesConverterInterface|null $uriVariablesConverter = null, + ?UriVariablesConverterInterface $uriVariablesConverter = null, ) { $this->uriVariablesConverter = $uriVariablesConverter; - - if ($processor instanceof CallableProcessor) { - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', WriteProcessor::class, self::class, $processor::class); - } - - if ($iriConverter instanceof ResourceMetadataCollectionFactoryInterface) { - $resourceMetadataCollectionFactory = $iriConverter; - } else { - $this->iriConverter = $iriConverter; - trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, IriConverterInterface::class); - } - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } @@ -82,107 +58,37 @@ public function __construct( */ public function onKernelView(ViewEvent $event): void { - $controllerResult = $event->getControllerResult(); $request = $event->getRequest(); $operation = $this->initializeOperation($request); - if (!($attributes = RequestAttributesExtractor::extractAttributes($request)) || !$attributes['persist']) { - return; - } - - if ($operation && (!$this->processor instanceof CallableProcessor && !$this->iriConverter)) { - if (null === $operation->canWrite()) { - $operation = $operation->withWrite(!$request->isMethodSafe()); - } - - $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; - if (!$uriVariables && !$operation instanceof Error && $operation instanceof HttpOperation) { - try { - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); - } catch (InvalidIdentifierException|InvalidUriVariableException $e) { - throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); - } - } - - $data = $this->processor->process($controllerResult, $operation, $uriVariables, [ - 'request' => $request, - 'uri_variables' => $uriVariables, - 'resource_class' => $operation->getClass(), - 'previous_data' => false === $operation->canRead() ? null : $request->attributes->get('previous_data'), - ]); - - if ($data) { - $request->attributes->set('original_data', $data); - } - - $event->setControllerResult($data); - - return; - } - - // API Platform 3.2 has a MainController where everything is handled by processors/providers - if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) { + if (!($attributes = RequestAttributesExtractor::extractAttributes($request)) || !$attributes['persist'] || !$operation) { return; } - if ( - $controllerResult instanceof Response - || $request->isMethodSafe() - || !($attributes = RequestAttributesExtractor::extractAttributes($request)) - ) { - return; - } - - if (!$attributes['persist'] || !($operation?->canWrite() ?? true)) { - return; + if (null === $operation->canWrite()) { + $operation = $operation->withWrite(!$request->isMethodSafe()); } - if (!$operation?->getProcessor()) { - return; + $uriVariables = $request->attributes->get('_api_uri_variables') ?? []; + if (!$uriVariables && !$operation instanceof Error && $operation instanceof HttpOperation) { + try { + $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $operation->getClass()); + } catch (InvalidIdentifierException|InvalidUriVariableException $e) { + throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); + } } - $context = [ - 'operation' => $operation, - 'resource_class' => $attributes['resource_class'], - 'previous_data' => $attributes['previous_data'] ?? null, - ]; + $data = $this->processor->process($event->getControllerResult(), $operation, $uriVariables, [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + 'previous_data' => false === $operation->canRead() ? null : $request->attributes->get('previous_data'), + ]); - try { - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); - } catch (InvalidIdentifierException $e) { - throw new NotFoundHttpException('Invalid identifier value or configuration.', $e); + if ($data) { + $request->attributes->set('original_data', $data); } - switch ($request->getMethod()) { - case 'PUT': - case 'PATCH': - case 'POST': - $persistResult = $this->processor->process($controllerResult, $operation, $uriVariables, $context); - - if ($persistResult) { - $controllerResult = $persistResult; - $event->setControllerResult($controllerResult); - } - - if ($controllerResult instanceof Response) { - break; - } - - $outputMetadata = $operation->getOutput() ?? ['class' => $attributes['resource_class']]; - $hasOutput = \is_array($outputMetadata) && \array_key_exists('class', $outputMetadata) && null !== $outputMetadata['class']; - if (!$hasOutput) { - break; - } - - if ($this->resourceClassResolver->isResourceClass($this->getObjectClass($controllerResult))) { - $request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromResource($controllerResult)); - } - - break; - case 'DELETE': - $this->processor->process($controllerResult, $operation, $uriVariables, $context); - $event->setControllerResult(null); - break; - } + $event->setControllerResult($data); } } diff --git a/src/Symfony/Maker/Resources/skeleton/StateProcessor.tpl.php b/src/Symfony/Maker/Resources/skeleton/StateProcessor.tpl.php index 6d5050ef5a7..662d56ccaec 100644 --- a/src/Symfony/Maker/Resources/skeleton/StateProcessor.tpl.php +++ b/src/Symfony/Maker/Resources/skeleton/StateProcessor.tpl.php @@ -8,8 +8,8 @@ class implements ProcessorInterface { - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { - // Handle the state + // Handle the state and return the inner processor } } diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index a2ad4a05f50..b1bb70a3d09 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -13,9 +13,6 @@ namespace ApiPlatform\Symfony\Routing; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Api\UriVariablesConverterInterface as LegacyUriVariablesConverterInterface; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\InvalidIdentifierException; @@ -55,7 +52,7 @@ final class IriConverter implements IriConverterInterface private $localOperationCache = []; private $localIdentifiersExtractorOperationCache = []; - public function __construct(private readonly ProviderInterface $provider, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, UriVariablesConverterInterface|LegacyUriVariablesConverterInterface|null $uriVariablesConverter = null, private readonly ?IriConverterInterface $decorated = null, private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null) + public function __construct(private readonly ProviderInterface $provider, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ?UriVariablesConverterInterface $uriVariablesConverter = null, private readonly ?IriConverterInterface $decorated = null, private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null) { $this->resourceClassResolver = $resourceClassResolver; $this->uriVariablesConverter = $uriVariablesConverter; diff --git a/tests/Documentation/Action/DocumentationActionTest.php b/src/Symfony/Tests/Action/DocumentationActionTest.php similarity index 55% rename from tests/Documentation/Action/DocumentationActionTest.php rename to src/Symfony/Tests/Action/DocumentationActionTest.php index 4914c90cb06..7fbec7e5288 100644 --- a/tests/Documentation/Action/DocumentationActionTest.php +++ b/src/Symfony/Tests/Action/DocumentationActionTest.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Documentation\Action; +namespace ApiPlatform\Symfony\Tests\Action; -use ApiPlatform\Documentation\Action\DocumentationAction; use ApiPlatform\Documentation\Documentation; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; @@ -23,11 +22,10 @@ use ApiPlatform\OpenApi\OpenApi; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Symfony\Action\DocumentationAction; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; /** @@ -42,48 +40,21 @@ public function testDocumentationAction(): void $openApi = new OpenApi(new Info('my api', '1.0.0'), [], new Paths()); $openApiFactoryProphecy = $this->prophesize(OpenApiFactoryInterface::class); $openApiFactoryProphecy->__invoke(Argument::any())->shouldBeCalled()->willReturn($openApi); - $requestProphecy = $this->prophesize(Request::class); - $requestProphecy->getMimeType('json')->willReturn('application/json'); - $requestProphecy->getRequestFormat('')->willReturn('json'); - $attributesProphecy = $this->prophesize(ParameterBagInterface::class); - $queryProphecy = $this->prophesize(ParameterBag::class); - $requestProphecy->attributes = $attributesProphecy->reveal(); - $requestProphecy->query = $queryProphecy->reveal(); - $requestProphecy->headers = $this->prophesize(ParameterBagInterface::class)->reveal(); - $requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1); - $queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1); - $queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1); - $attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1); - $attributesProphecy->get('_format')->willReturn(null)->shouldBeCalledTimes(1); - $attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1); - $documentation = new DocumentationAction($this->prophesize(ResourceNameCollectionFactoryInterface::class)->reveal(), 'my api', '', '1.0.0', $openApiFactoryProphecy->reveal()); - $this->assertInstanceOf(OpenApi::class, $documentation($requestProphecy->reveal())); + $this->assertInstanceOf(OpenApi::class, $documentation( + new Request(query: ['api_gateway' => true, 'spec_version' => '3.1.0'], server: ['REQUEST_URI' => '/api'], attributes: ['_format' => null, '_api_normalization_context' => ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0']]) + )); } public function testDocumentationActionWithoutOpenApiFactory(): void { $openApiFactoryProphecy = $this->prophesize(OpenApiFactoryInterface::class); $openApiFactoryProphecy->__invoke(Argument::any())->shouldNotBeCalled(); - $requestProphecy = $this->prophesize(Request::class); - $requestProphecy->getRequestFormat('')->willReturn('json'); - $requestProphecy->headers = $this->prophesize(ParameterBagInterface::class)->reveal(); - $requestProphecy->getMimeType('json')->willReturn('application/json'); - $attributesProphecy = $this->prophesize(ParameterBagInterface::class); - $attributesProphecy->get('_format')->willReturn(null)->shouldBeCalledTimes(1); - $queryProphecy = $this->prophesize(ParameterBag::class); - $requestProphecy->attributes = $attributesProphecy->reveal(); - $requestProphecy->query = $queryProphecy->reveal(); - $requestProphecy->getBaseUrl()->willReturn('/api')->shouldBeCalledTimes(1); - $queryProphecy->getBoolean('api_gateway')->willReturn(true)->shouldBeCalledTimes(1); - $queryProphecy->get('spec_version')->willReturn('3.1.0')->shouldBeCalledTimes(1); - $attributesProphecy->get('_api_normalization_context', [])->willReturn(['foo' => 'bar'])->shouldBeCalledTimes(1); - $attributesProphecy->set('_api_normalization_context', ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0'])->shouldBeCalledTimes(1); $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies']))->shouldBeCalled(); $documentation = new DocumentationAction($resourceNameCollectionFactoryProphecy->reveal(), 'my api', '', '1.0.0'); - $this->assertInstanceOf(Documentation::class, $documentation($requestProphecy->reveal())); + $this->assertInstanceOf(Documentation::class, $documentation(new Request(query: ['api_gateway' => true, 'spec_version' => '3.1.0'], server: ['REQUEST_URI' => '/api'], attributes: ['_format' => null, '_api_normalization_context' => ['foo' => 'bar', 'base_url' => '/api', 'api_gateway' => true, 'spec_version' => '3.1.0']]))); } public static function getOpenApiContentTypes(): array @@ -91,17 +62,14 @@ public static function getOpenApiContentTypes(): array return [['application/json'], ['application/html']]; } - /** - * @dataProvider getOpenApiContentTypes - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getOpenApiContentTypes')] public function testGetOpenApi($contentType): void { $request = new Request(server: ['CONTENT_TYPE' => $contentType]); $openApiFactory = $this->createMock(OpenApiFactoryInterface::class); - $openApiFactory->expects($this->once())->method('__invoke')->willReturn(new OpenApi(new Info('a', 'v'), [], new Paths())); $resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class); $provider = $this->createMock(ProviderInterface::class); - $provider->expects($this->once())->method('provide')->willReturnCallback(fn ($operation, $uriVariables, $context) => $operation->getProvider()(...\func_get_args())); + $provider->expects($this->once())->method('provide')->willReturnCallback(fn ($operation, $uriVariables, $context) => new OpenApi(new Info('title', '1.0.0'), [], new Paths())); $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->once())->method('process')->willReturnArgument(0); $entrypoint = new DocumentationAction($resourceNameCollectionFactory, provider: $provider, processor: $processor, openApiFactory: $openApiFactory); diff --git a/tests/Action/EntrypointActionTest.php b/src/Symfony/Tests/Action/EntrypointActionTest.php similarity index 66% rename from tests/Action/EntrypointActionTest.php rename to src/Symfony/Tests/Action/EntrypointActionTest.php index def1278bb54..4bdac16617e 100644 --- a/tests/Action/EntrypointActionTest.php +++ b/src/Symfony/Tests/Action/EntrypointActionTest.php @@ -11,32 +11,22 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Action; +namespace ApiPlatform\Symfony\Tests\Action; -use ApiPlatform\Action\EntrypointAction; -use ApiPlatform\Api\Entrypoint; +use ApiPlatform\Documentation\Action\EntrypointAction; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\HttpFoundation\Request; /** * @author Amrouche Hamza */ class EntrypointActionTest extends TestCase { - use ProphecyTrait; - - public function testGetEntrypoint(): void - { - $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); - $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummies'])); - $entrypoint = new EntrypointAction($resourceNameCollectionFactoryProphecy->reveal()); - $this->assertEquals(new Entrypoint(new ResourceNameCollection(['dummies'])), $entrypoint()); - } - public function testGetEntrypointWithProviderProcessor(): void { $expected = new Entrypoint(new ResourceNameCollection(['dummies'])); @@ -47,6 +37,6 @@ public function testGetEntrypointWithProviderProcessor(): void $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->once())->method('process')->willReturnArgument(0); $entrypoint = new EntrypointAction($resourceNameCollectionFactory, $provider, $processor); - $this->assertEquals($expected, $entrypoint()); + $this->assertEquals($expected, $entrypoint(Request::create('/'))); } } diff --git a/tests/Action/PlaceholderActionTest.php b/src/Symfony/Tests/Action/PlaceholderActionTest.php similarity index 86% rename from tests/Action/PlaceholderActionTest.php rename to src/Symfony/Tests/Action/PlaceholderActionTest.php index e883ffec318..7bdca6eb6d1 100644 --- a/tests/Action/PlaceholderActionTest.php +++ b/src/Symfony/Tests/Action/PlaceholderActionTest.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Action; +namespace ApiPlatform\Symfony\Tests\Action; -use ApiPlatform\Action\PlaceholderAction; +use ApiPlatform\Symfony\Action\PlaceholderAction; use PHPUnit\Framework\TestCase; /** diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 3997760d60e..2a2fe0f197c 100644 --- a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Symfony\Tests\Bundle\DependencyInjection; -use ApiPlatform\Action\NotFoundAction; use ApiPlatform\Metadata\Exception\ExceptionInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IdentifiersExtractorInterface; @@ -22,9 +21,10 @@ use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\Filter\GroupFilter; use ApiPlatform\Serializer\Filter\PropertyFilter; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; use ApiPlatform\State\Pagination\Pagination; use ApiPlatform\State\Pagination\PaginationOptions; +use ApiPlatform\State\SerializerContextBuilderInterface; +use ApiPlatform\Symfony\Action\NotFoundAction; use ApiPlatform\Symfony\Bundle\DependencyInjection\ApiPlatformExtension; use ApiPlatform\Tests\Fixtures\TestBundle\TestBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; @@ -110,9 +110,7 @@ class ApiPlatformExtensionTest extends TestCase 'graphql' => [ 'graphql_playground' => ['enabled' => false], ], - 'event_listeners_backward_compatibility_layer' => false, 'use_symfony_listeners' => false, - 'keep_legacy_inflector' => false, ]]; private ContainerBuilder $container; @@ -178,9 +176,7 @@ public function testCommonConfiguration(): void $services = [ 'api_platform.action.documentation', 'api_platform.action.entrypoint', - 'api_platform.action.exception', 'api_platform.action.not_found', - 'api_platform.action.placeholder', 'api_platform.api.identifiers_extractor', 'api_platform.filter_locator', 'api_platform.negotiator', @@ -222,12 +218,6 @@ public function testCommonConfiguration(): void SerializerContextBuilderInterface::class, Pagination::class, PaginationOptions::class, - 'api_platform.action.delete_item', - 'api_platform.action.get_collection', - 'api_platform.action.get_item', - 'api_platform.action.patch_item', - 'api_platform.action.post_collection', - 'api_platform.action.put_item', 'api_platform.identifiers_extractor', 'api_platform.iri_converter', 'api_platform.path_segment_name_generator', @@ -284,6 +274,15 @@ public function testEventListenersConfiguration(): void 'api_platform.state_processor.serialize', ]; - $this->assertContainerHas($services, []); + $aliases = [ + 'api_platform.action.delete_item', + 'api_platform.action.get_collection', + 'api_platform.action.get_item', + 'api_platform.action.patch_item', + 'api_platform.action.post_collection', + 'api_platform.action.put_item', + ]; + + $this->assertContainerHas($services, $aliases); } } diff --git a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php index 11a70b10527..36280edf59e 100644 --- a/src/Symfony/Tests/EventListener/AddFormatListenerTest.php +++ b/src/Symfony/Tests/EventListener/AddFormatListenerTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Symfony\Tests\EventListener; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -70,6 +71,7 @@ public function testNoCallProvider(...$attributes): void $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->never())->method('provide'); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection($attributes['_api_resource_class'] ?? '')); $listener = new AddFormatListener($provider, $metadata); $listener->onKernelRequest( new RequestEvent( @@ -83,10 +85,34 @@ public function testNoCallProvider(...$attributes): void public static function provideNonApiAttributes(): array { return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], ['_api_respond' => false, '_api_operation_name' => 'dummy'], [], ]; } + + #[DataProvider('provideOperationNotFound')] + public function testNoOperation(...$attributes): void + { + $this->expectException(OperationNotFoundException::class); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->never())->method('provide'); + $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection($attributes['_api_resource_class'] ?? '')); + $listener = new AddFormatListener($provider, $metadata); + $listener->onKernelRequest( + new RequestEvent( + $this->createStub(HttpKernelInterface::class), + new Request([], [], $attributes), + HttpKernelInterface::MAIN_REQUEST + ) + ); + } + + public static function provideOperationNotFound(): array + { + return [ + ['_api_resource_class' => 'dummy'], + ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], + ]; + } } diff --git a/src/Symfony/Tests/EventListener/DeserializeListenerTest.php b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php index 4a2a66ba538..7cb70f4778c 100644 --- a/src/Symfony/Tests/EventListener/DeserializeListenerTest.php +++ b/src/Symfony/Tests/EventListener/DeserializeListenerTest.php @@ -74,6 +74,7 @@ public function testNoCallProvider(...$attributes): void $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->never())->method('provide'); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $listener = new DeserializeListener($provider, $metadata); $listener->onKernelRequest( new RequestEvent( @@ -84,6 +85,14 @@ public function testNoCallProvider(...$attributes): void ); } + public static function provideNonApiAttributes(): array + { + return [ + ['_api_receive' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } + public function testDeserializeFalse(): void { $provider = $this->createMock(ProviderInterface::class); @@ -116,14 +125,4 @@ public function testDeserializeNullWithGetMethod(): void ) ); } - - public static function provideNonApiAttributes(): array - { - return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], - ['_api_receive' => false, '_api_operation_name' => 'dummy'], - [], - ]; - } } diff --git a/src/Symfony/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Tests/EventListener/ErrorListenerTest.php index f9f1e9e21cf..09b8a990248 100644 --- a/src/Symfony/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Tests/EventListener/ErrorListenerTest.php @@ -30,6 +30,11 @@ class ErrorListenerTest extends TestCase { + protected function tearDown(): void + { + restore_exception_handler(); + } + public function testDuplicateException(): void { $exception = new \Exception(); @@ -55,9 +60,9 @@ public function testDuplicateException(): void }); $request = Request::create('/'); - $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); + $request->attributes->set('_api_operation', new Get()); $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); - $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver, problemCompliantErrors: true); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonproblem' => ['application/problem+json']], [], null, $resourceClassResolver); $errorListener->onKernelException($exceptionEvent); } @@ -85,7 +90,7 @@ public function testDuplicateExceptionWithHydra(): void return new Response(); }); $request = Request::create('/'); - $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); + $request->attributes->set('_api_operation', new Get()); $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory, ['jsonld' => ['application/ld+json']], [], null, $resourceClassResolver); $errorListener->onKernelException($exceptionEvent); @@ -130,7 +135,7 @@ public function testDuplicateExceptionWithErrorResource(): void return new Response(); }); $request = Request::create('/'); - $request->attributes->set('_api_operation', new Get(extraProperties: ['rfc_7807_compliant_errors' => true])); + $request->attributes->set('_api_operation', new Get()); $exceptionEvent = new ExceptionEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST, $exception); $identifiersExtractor = $this->createStub(IdentifiersExtractorInterface::class); $identifiersExtractor->method('getIdentifiersFromItem')->willReturn(['id' => 1]); diff --git a/src/Symfony/Tests/EventListener/QueryParameterValidateListenerTest.php b/src/Symfony/Tests/EventListener/QueryParameterValidateListenerTest.php deleted file mode 100644 index 7362cbe19fe..00000000000 --- a/src/Symfony/Tests/EventListener/QueryParameterValidateListenerTest.php +++ /dev/null @@ -1,258 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Tests\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\ParameterValidator\Exception\ValidationException; -use ApiPlatform\ParameterValidator\ParameterValidator; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\Symfony\EventListener\QueryParameterValidateListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -class QueryParameterValidateListenerTest extends TestCase -{ - use ProphecyTrait; - - private QueryParameterValidateListener $testedInstance; - private ObjectProphecy $queryParameterValidator; - - /** - * @group legacy - * unsafe method should not use filter validations. - */ - public function testOnKernelRequestWithUnsafeMethod(): void - { - $this->setUpWithFilters(); - - $request = new Request(); - $request->setMethod('POST'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $this->queryParameterValidator->validateFilters(Argument::cetera())->shouldNotBeCalled(); - - $this->testedInstance->onKernelRequest($eventProphecy->reveal()); - } - - /** - * @group legacy - */ - public function testDoNotValidateWhenDisabledGlobally(): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(queryParameterValidationEnabled: false), - ]), - ])); - - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - - $queryParameterValidator = $this->prophesize(ParameterValidator::class); - $queryParameterValidator->validateFilters(Argument::cetera())->shouldNotBeCalled(); - - $listener = new QueryParameterValidateListener( - $queryParameterValidator->reveal(), - $resourceMetadataFactoryProphecy->reveal(), - ); - - $listener->onKernelRequest($eventProphecy->reveal()); - } - - /** - * @group legacy - */ - public function testDoNotValidateWhenDisabledInOperationAttribute(): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(queryParameterValidationEnabled: false), - ]), - ])); - - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - - $queryParameterValidator = $this->prophesize(ParameterValidator::class); - $queryParameterValidator->validateFilters(Argument::cetera())->shouldNotBeCalled(); - - $listener = new QueryParameterValidateListener( - $queryParameterValidator->reveal(), - $resourceMetadataFactoryProphecy->reveal(), - ); - - $listener->onKernelRequest($eventProphecy->reveal()); - } - - /** - * If the tested filter is non-existent, then nothing should append. - * - * @group legacy - */ - public function testOnKernelRequestWithWrongFilter(): void - { - $this->setUpWithFilters(['some_inexistent_filter']); - - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $request->setMethod('GET'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $this->queryParameterValidator->validateFilters(Dummy::class, ['some_inexistent_filter'], [])->shouldBeCalled(); - - $this->testedInstance->onKernelRequest($eventProphecy->reveal()); - } - - /** - * if the required parameter is not set, throw an ValidationException. - * - * @group legacy - */ - public function testOnKernelRequestWithRequiredFilterNotSet(): void - { - $this->setUpWithFilters(['some_filter']); - - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $request->setMethod('GET'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $this->queryParameterValidator - ->validateFilters(Dummy::class, ['some_filter'], []) - ->shouldBeCalled() - ->willThrow(new ValidationException(['Query parameter "required" is required'])); - $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Query parameter "required" is required'); - $this->testedInstance->onKernelRequest($eventProphecy->reveal()); - } - - /** - * if the required parameter is set, no exception should be thrown. - * - * @group legacy - */ - public function testOnKernelRequestWithRequiredFilter(): void - { - $this->setUpWithFilters(['some_filter']); - - $request = new Request( - [], - [], - ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get'], - [], - [], - ['QUERY_STRING' => 'required=foo'] - ); - $request->setMethod('GET'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $this->queryParameterValidator - ->validateFilters(Dummy::class, ['some_filter'], ['required' => 'foo']) - ->shouldBeCalled(); - - $this->testedInstance->onKernelRequest($eventProphecy->reveal()); - } - - /** - * if parameter use_symfony_listeners is true. - * - * @group legacy - */ - public function testDoNothingWhenListenersDisabled(): void - { - $parameterValidator = $this->prophesize(ProviderInterface::class); - $parameterValidator->provide()->shouldNotBeCalled(); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => new Get(name: 'get')])]))->shouldBeCalled(); - - $listener = new QueryParameterValidateListener($parameterValidator->reveal(), $factory->reveal()); - - $event = new RequestEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_platform_disable_listeners' => true]), - HttpKernelInterface::MAIN_REQUEST, - ); - - $listener->onKernelRequest($event); - } - - private function setUpWithFilters(array $filters = []): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new GetCollection(filters: $filters), - ]), - ])); - - $this->queryParameterValidator = $this->prophesize(ParameterValidator::class); - - $this->testedInstance = new QueryParameterValidateListener( - $this->queryParameterValidator->reveal(), - $resourceMetadataFactoryProphecy->reveal(), - ); - } - - public function testOnKernelRequest(): void - { - $request = new Request( - [], - [], - ['_api_resource_class' => Dummy::class, '_api_operation' => new GetCollection(), '_api_operation_name' => 'get'], - [], - [], - ['QUERY_STRING' => 'required=foo'] - ); - $request->setMethod('GET'); - - $event = $this->createMock(RequestEvent::class); - $event->expects($this->any()) - ->method('getRequest') - ->willReturn($request); - - $resourceMetadataFactoryProphecy = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); - $provider = $this->createMock(ProviderInterface::class); - $provider->expects($this->once()) - ->method('provide'); - - $qp = new QueryParameterValidateListener( - $provider, - $resourceMetadataFactoryProphecy, - ); - $qp->onKernelRequest($event); - } -} diff --git a/src/Symfony/Tests/EventListener/ReadListenerTest.php b/src/Symfony/Tests/EventListener/ReadListenerTest.php index 26c05ae1fbd..e50f4c67225 100644 --- a/src/Symfony/Tests/EventListener/ReadListenerTest.php +++ b/src/Symfony/Tests/EventListener/ReadListenerTest.php @@ -74,6 +74,7 @@ public function testNoCallProvider(...$attributes): void $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->never())->method('provide'); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $listener = new ReadListener($provider, $metadata); $listener->onKernelRequest( new RequestEvent( @@ -84,6 +85,14 @@ public function testNoCallProvider(...$attributes): void ); } + public static function provideNonApiAttributes(): array + { + return [ + ['_api_receive' => false, '_api_operation_name' => 'dummy'], + [], + ]; + } + public function testReadFalse(): void { $operation = new Get(read: false); @@ -110,7 +119,7 @@ public function testReadWithUriVariables(): void $uriVariablesConverter = $this->createMock(UriVariablesConverterInterface::class); $uriVariablesConverter->expects($this->once())->method('convert')->with(['id' => '3'], 'class')->willReturn(['id' => 3]); $request = new Request([], [], ['_api_operation' => $operation, '_api_operation_name' => 'operation', '_api_resource_class' => 'class', 'id' => '3']); - $listener = new ReadListener($provider, $metadata, null, $uriVariablesConverter); + $listener = new ReadListener($provider, $metadata, $uriVariablesConverter); $listener->onKernelRequest( new RequestEvent( $this->createStub(HttpKernelInterface::class), @@ -137,14 +146,4 @@ public function testReadNullWithPostMethod(): void ) ); } - - public static function provideNonApiAttributes(): array - { - return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], - ['_api_receive' => false, '_api_operation_name' => 'dummy'], - [], - ]; - } } diff --git a/src/Symfony/Tests/EventListener/RespondListenerTest.php b/src/Symfony/Tests/EventListener/RespondListenerTest.php index d897198dae3..a78412968fd 100644 --- a/src/Symfony/Tests/EventListener/RespondListenerTest.php +++ b/src/Symfony/Tests/EventListener/RespondListenerTest.php @@ -41,7 +41,7 @@ public function testFetchOperation(): void ])); $request = new Request([], [], ['_api_operation_name' => 'operation', '_api_resource_class' => 'class']); - $listener = new RespondListener($metadata, $processor); + $listener = new RespondListener($processor, $metadata); $listener->onKernelView( new ViewEvent( $this->createStub(HttpKernelInterface::class), @@ -59,7 +59,7 @@ public function testCallProcessor(): void $processor->expects($this->once())->method('process')->willReturn(new Response()); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); $request = new Request([], [], ['_api_operation' => new Get(), '_api_operation_name' => 'operation', '_api_resource_class' => 'class']); - $listener = new RespondListener($metadata, $processor); + $listener = new RespondListener($processor, $metadata); $listener->onKernelView( new ViewEvent( $this->createStub(HttpKernelInterface::class), @@ -81,7 +81,7 @@ public function testCallProcessorContext(): void $processor->expects($this->once())->method('process') ->with($controllerResult, $operation, $uriVariables, ['request' => $request, 'uri_variables' => $uriVariables, 'resource_class' => 'class', 'original_data' => $originalData])->willReturn(new Response()); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); - $listener = new RespondListener($metadata, $processor); + $listener = new RespondListener($processor, $metadata); $listener->onKernelView( new ViewEvent( $this->createStub(HttpKernelInterface::class), @@ -99,8 +99,9 @@ public function testNoCallProcessor(...$attributes): void $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->never())->method('process')->willReturn(new Response()); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $request = new Request([], [], $attributes); - $listener = new RespondListener($metadata, $processor); + $listener = new RespondListener($processor, $metadata); $listener->onKernelView( new ViewEvent( $this->createStub(HttpKernelInterface::class), @@ -114,8 +115,6 @@ public function testNoCallProcessor(...$attributes): void public static function provideNonApiAttributes(): array { return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], ['_api_respond' => false, '_api_operation_name' => 'dummy'], [], ]; diff --git a/src/Symfony/Tests/EventListener/SerializeListenerTest.php b/src/Symfony/Tests/EventListener/SerializeListenerTest.php index 2ffdae3ebda..4ad6834c34b 100644 --- a/src/Symfony/Tests/EventListener/SerializeListenerTest.php +++ b/src/Symfony/Tests/EventListener/SerializeListenerTest.php @@ -98,6 +98,7 @@ public function testNoCallProcessor(...$attributes): void $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->never())->method('process')->willReturn(new Response()); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $request = new Request([], [], $attributes); $listener = new SerializeListener($processor, $metadata); $listener->onKernelView( @@ -113,8 +114,6 @@ public function testNoCallProcessor(...$attributes): void public static function provideNonApiAttributes(): array { return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], ['_api_respond' => false, '_api_operation_name' => 'dummy'], [], ]; diff --git a/src/Symfony/Tests/EventListener/ValidateListenerTest.php b/src/Symfony/Tests/EventListener/ValidateListenerTest.php index f686b5e40cf..bd0e38d23f2 100644 --- a/src/Symfony/Tests/EventListener/ValidateListenerTest.php +++ b/src/Symfony/Tests/EventListener/ValidateListenerTest.php @@ -143,6 +143,7 @@ public function testNoCallprovider(...$attributes): void $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->never())->method('provide'); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $request = new Request([], [], $attributes); $listener = new ValidateListener($provider, $metadata); $listener->onKernelView( @@ -158,8 +159,6 @@ public function testNoCallprovider(...$attributes): void public static function provideNonApiAttributes(): array { return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], ['_api_respond' => false, '_api_operation_name' => 'dummy'], [], ]; diff --git a/src/Symfony/Tests/EventListener/WriteListenerTest.php b/src/Symfony/Tests/EventListener/WriteListenerTest.php index 98280b2f3d8..921aa4cb857 100644 --- a/src/Symfony/Tests/EventListener/WriteListenerTest.php +++ b/src/Symfony/Tests/EventListener/WriteListenerTest.php @@ -104,6 +104,7 @@ public function testNoCallProcessor(...$attributes): void $processor = $this->createMock(ProcessorInterface::class); $processor->expects($this->never())->method('process')->willReturn(new Response()); $metadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); + $metadata->method('create')->willReturn(new ResourceMetadataCollection('class')); $request = new Request([], [], $attributes); $listener = new WriteListener($processor, $metadata); $listener->onKernelView( @@ -119,8 +120,6 @@ public function testNoCallProcessor(...$attributes): void public static function provideNonApiAttributes(): array { return [ - ['_api_resource_class' => 'dummy'], - ['_api_resource_class' => 'dummy', '_api_operation_name' => 'dummy'], ['_api_persist' => false, '_api_operation_name' => 'dummy'], [], ]; diff --git a/src/Symfony/Tests/Validator/State/QueryParameterValidateProviderTest.php b/src/Symfony/Tests/Validator/State/QueryParameterValidateProviderTest.php deleted file mode 100644 index 30a14d5c3f5..00000000000 --- a/src/Symfony/Tests/Validator/State/QueryParameterValidateProviderTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Tests\Validator\State; - -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\ParameterValidator\ParameterValidator; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\Symfony\Validator\State\QueryParameterValidateProvider; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\ServerBag; - -class QueryParameterValidateProviderTest extends TestCase -{ - public function testValidate(): void - { - $filters = ['test']; - $operation = new GetCollection(filters: $filters, class: 'foo'); - $request = $this->createMock(Request::class); - $request->server = $this->createMock(ServerBag::class); - $request->server->method('get')->with('QUERY_STRING')->willReturn('foo=bar'); - $request->method('isMethodSafe')->willReturn(true); - $request->method('getMethod')->willReturn('GET'); - $context = ['request' => $request]; - $obj = new \stdClass(); - $decorated = $this->createMock(ProviderInterface::class); - $decorated->method('provide')->willReturn($obj); - $validator = $this->createMock(ParameterValidator::class); - $validator->expects($this->once())->method('validateFilters')->with('foo', $filters, ['foo' => 'bar']); - $provider = new QueryParameterValidateProvider($decorated, $validator); - $provider->provide($operation, [], $context); - } -} diff --git a/src/Api/FormatMatcher.php b/src/Symfony/Util/FormatMatcher.php similarity index 97% rename from src/Api/FormatMatcher.php rename to src/Symfony/Util/FormatMatcher.php index 957fd1c7dc5..d0d5eb8153c 100644 --- a/src/Api/FormatMatcher.php +++ b/src/Symfony/Util/FormatMatcher.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Api; +namespace ApiPlatform\Symfony\Util; /** * Matches a mime type to a format. diff --git a/src/Symfony/Validator/EventListener/ValidationExceptionListener.php b/src/Symfony/Validator/EventListener/ValidationExceptionListener.php deleted file mode 100644 index e9896292840..00000000000 --- a/src/Symfony/Validator/EventListener/ValidationExceptionListener.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Validator\EventListener; - -use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\ParameterValidator\Exception\ValidationException as ParameterValidationException; -use ApiPlatform\Symfony\EventListener\ExceptionListener; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as SymfonyConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Util\ErrorFormatGuesser; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Validator\Exception\ValidationException; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * Handles validation errors. - * TODO: remove this class. - * - * @deprecated - * - * @author Kévin Dunglas - */ -final class ValidationExceptionListener -{ - public function __construct(private readonly SerializerInterface $serializer, private readonly array $errorFormats, private readonly array $exceptionToStatus = [], private readonly ?ExceptionListener $exceptionListener = null) - { - } - - /** - * Returns a list of violations normalized in the Hydra format. - */ - public function onKernelException(ExceptionEvent $event): void - { - // API Platform 3.2 handles every exception through the exception listener so we just skip this one - if ($this->exceptionListener) { - return; - } - - trigger_deprecation('api-platform', '3.2', \sprintf('The class "%s" is deprecated and will be removed in 4.x.', __CLASS__)); - - $exception = $event->getThrowable(); - $hasConstraintViolationList = ($exception instanceof ConstraintViolationListAwareExceptionInterface || $exception instanceof SymfonyConstraintViolationListAwareExceptionInterface); - if (!$hasConstraintViolationList && !$exception instanceof FilterValidationException && !$exception instanceof ParameterValidationException) { - return; - } - - $exceptionClass = $exception::class; - $statusCode = Response::HTTP_UNPROCESSABLE_ENTITY; - - foreach ($this->exceptionToStatus as $class => $status) { - if (is_a($exceptionClass, $class, true)) { - $statusCode = $status; - - break; - } - } - - $format = ErrorFormatGuesser::guessErrorFormat($event->getRequest(), $this->errorFormats); - - $context = []; - if ($exception instanceof ValidationException && ($errorTitle = $exception->getErrorTitle())) { - $context['title'] = $errorTitle; - } - - $event->setResponse(new Response( - $this->serializer->serialize($hasConstraintViolationList ? $exception->getConstraintViolationList() : $exception, $format['key'], $context), - $statusCode, - [ - 'Content-Type' => \sprintf('%s; charset=utf-8', $format['value'][0]), - 'X-Content-Type-Options' => 'nosniff', - 'X-Frame-Options' => 'deny', - ] - )); - } -} diff --git a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php b/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php deleted file mode 100644 index cf137c1ea9b..00000000000 --- a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Validator\Exception; - -use ApiPlatform\Metadata\Exception\ExceptionInterface; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * An exception which has a constraint violation list. - * - * @deprecated use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface - */ -interface ConstraintViolationListAwareExceptionInterface extends ExceptionInterface -{ - /** - * Gets constraint violations related to this exception. - */ - public function getConstraintViolationList(): ConstraintViolationListInterface; -} diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php deleted file mode 100644 index 6100c94b72d..00000000000 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Validator\Exception; - -use ApiPlatform\JsonLd\ContextBuilderInterface; -use ApiPlatform\Metadata\Error as ErrorOperation; -use ApiPlatform\Metadata\ErrorResource; -use ApiPlatform\Metadata\Exception\HttpExceptionInterface; -use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Validator\Exception\ValidationException as BaseValidationException; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; -use Symfony\Component\WebLink\Link; - -/** - * Thrown when a validation error occurs. - * - * @author Kévin Dunglas - * - * @deprecated since API Platform 3.3, use {@see BaseValidationException} instead - */ -#[ErrorResource( - uriTemplate: '/validation_errors/{id}', - status: 422, - openapi: false, - uriVariables: ['id'], - provider: 'api_platform.validator.state.error_provider', - shortName: 'ConstraintViolationList', - operations: [ - new ErrorOperation( - name: '_api_validation_errors_problem', - routeName: 'api_validation_errors', - outputFormats: ['json' => ['application/problem+json']], - normalizationContext: ['groups' => ['json'], - 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, - ] - ), - new ErrorOperation( - name: '_api_validation_errors_hydra', - routeName: 'api_validation_errors', - outputFormats: ['jsonld' => ['application/problem+json']], - links: [new Link(rel: ContextBuilderInterface::JSONLD_NS.'error', href: 'http://www.w3.org/ns/hydra/error')], - normalizationContext: [ - 'groups' => ['jsonld'], - 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, - ] - ), - new ErrorOperation( - name: '_api_validation_errors_jsonapi', - routeName: 'api_validation_errors', - outputFormats: ['jsonapi' => ['application/vnd.api+json']], - normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true, 'rfc_7807_compliant_errors' => true] - ), - new ErrorOperation( - name: '_api_validation_errors', - routeName: 'api_validation_errors' - ), - ], - graphQlOperations: [] -)] -final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface -{ -} diff --git a/src/Symfony/Validator/Serializer/ValidationExceptionNormalizer.php b/src/Symfony/Validator/Serializer/ValidationExceptionNormalizer.php index fb2add5abc8..f51176475a3 100644 --- a/src/Symfony/Validator/Serializer/ValidationExceptionNormalizer.php +++ b/src/Symfony/Validator/Serializer/ValidationExceptionNormalizer.php @@ -13,15 +13,13 @@ namespace ApiPlatform\Symfony\Validator\Serializer; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; -class ValidationExceptionNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +class ValidationExceptionNormalizer implements NormalizerInterface { public function __construct(private readonly NormalizerInterface $decorated, private readonly ?NameConverterInterface $nameConverter) { @@ -55,20 +53,6 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $data instanceof ValidationException && $this->decorated->supportsNormalization($data, $format, $context); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return false; - } - public function getSupportedTypes($format): array { return [ValidationException::class => false]; diff --git a/src/Symfony/Validator/State/ErrorProvider.php b/src/Symfony/Validator/State/ErrorProvider.php index ea0bf84fd32..3b813f377ec 100644 --- a/src/Symfony/Validator/State/ErrorProvider.php +++ b/src/Symfony/Validator/State/ErrorProvider.php @@ -16,6 +16,8 @@ use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; /** * @internal @@ -26,7 +28,7 @@ public function __construct() { } - public function provide(Operation $operation, array $uriVariables = [], array $context = []): \Throwable + public function provide(Operation $operation, array $uriVariables = [], array $context = []): ConstraintViolationListInterface|\Throwable { if (!($request = $context['request'] ?? null) || !$operation instanceof HttpOperation) { throw new \RuntimeException('Not an HTTP request'); @@ -35,6 +37,10 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $exception = $request->attributes->get('exception'); $exception->setStatus($operation->getStatus()); + if ('jsonapi' === $request->getRequestFormat() && $exception instanceof ConstraintViolationListAwareExceptionInterface) { + return $exception->getConstraintViolationList(); + } + return $exception; } } diff --git a/src/Symfony/Validator/State/ParameterValidatorProvider.php b/src/Symfony/Validator/State/ParameterValidatorProvider.php index b88d3c217c9..faaa9715bca 100644 --- a/src/Symfony/Validator/State/ParameterValidatorProvider.php +++ b/src/Symfony/Validator/State/ParameterValidatorProvider.php @@ -40,7 +40,7 @@ public function __construct( public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { - if (!($request = $context['request']) instanceof Request) { + if (!($request = $context['request'] ?? null) instanceof Request) { return $this->decorated->provide($operation, $uriVariables, $context); } diff --git a/src/Symfony/Validator/State/QueryParameterValidateProvider.php b/src/Symfony/Validator/State/QueryParameterValidateProvider.php deleted file mode 100644 index 1b9dbd44071..00000000000 --- a/src/Symfony/Validator/State/QueryParameterValidateProvider.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Symfony\Validator\State; - -use ApiPlatform\Doctrine\Orm\State\Options; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\ParameterValidator\ParameterValidator; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\State\Util\RequestParser; - -/** - * @deprecated the query parameter validator is deprecated - */ -final class QueryParameterValidateProvider implements ProviderInterface -{ - public function __construct(private readonly ?ProviderInterface $decorated, private readonly ParameterValidator $parameterValidator) - { - } - - public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null - { - if ( - !$operation instanceof HttpOperation - || !($request = $context['request'] ?? null) - || !$request->isMethodSafe() - || 'GET' !== $request->getMethod() - ) { - return $this->decorated?->provide($operation, $uriVariables, $context); - } - - if (!($operation->getExtraProperties()['use_legacy_parameter_validator'] ?? true)) { - return $this->decorated?->provide($operation, $uriVariables, $context); - } - - if (!($operation->getQueryParameterValidationEnabled() ?? true) || !$operation instanceof CollectionOperationInterface) { - return $this->decorated?->provide($operation, $uriVariables, $context); - } - - $queryString = RequestParser::getQueryString($request); - $queryParameters = $queryString ? RequestParser::parseRequestParams($queryString) : []; - $class = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { - $class = $options->getEntityClass(); - } - - $this->parameterValidator->validateFilters($class, $operation->getFilters() ?? [], $queryParameters); - - return $this->decorated?->provide($operation, $uriVariables, $context); - } -} diff --git a/src/Symfony/Validator/Validator.php b/src/Symfony/Validator/Validator.php index 9580e008bd2..a0645ab9968 100644 --- a/src/Symfony/Validator/Validator.php +++ b/src/Symfony/Validator/Validator.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Symfony\Validator; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; use ApiPlatform\Validator\Exception\ValidationException; use ApiPlatform\Validator\ValidatorInterface; use Psr\Container\ContainerInterface; @@ -24,12 +23,10 @@ * Validates an item using the Symfony validator component. * * @author Kévin Dunglas - * - * @final */ -class Validator implements ValidatorInterface +final class Validator implements ValidatorInterface { - public function __construct(private readonly SymfonyValidatorInterface $validator, private readonly ?ContainerInterface $container = null, private readonly ?bool $legacyValidationException = true) + public function __construct(private readonly SymfonyValidatorInterface $validator, private readonly ?ContainerInterface $container = null) { } @@ -58,9 +55,6 @@ public function validate(object $data, array $context = []): void $violations = $this->validator->validate($data, null, $validationGroups); if (0 !== \count($violations)) { - if (true === $this->legacyValidationException) { - throw new LegacyValidationException($violations); - } throw new ValidationException($violations); } } diff --git a/src/Symfony/composer.json b/src/Symfony/composer.json index 97a6fba5b27..ac70c86a0d5 100644 --- a/src/Symfony/composer.json +++ b/src/Symfony/composer.json @@ -3,10 +3,16 @@ "description": "Symfony API Platform integration", "type": "symfony-bundle", "keywords": [ - "API", - "symfony", + "Symfony", "REST", - "GraphQL" + "GraphQL", + "API", + "JSON-LD", + "Hydra", + "JSONAPI", + "OpenAPI", + "HAL", + "Swagger" ], "homepage": "https://api-platform.com", "license": "MIT", @@ -22,7 +28,7 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/documentation": "^3.4 || ^4.0", "api-platform/http-cache": "^3.4 || ^4.0", "api-platform/json-schema": "^3.4 || ^4.0", @@ -33,27 +39,26 @@ "api-platform/state": "^3.4 || ^4.0", "api-platform/validator": "^3.4 || ^4.0", "api-platform/openapi": "^3.4 || ^4.0", - "symfony/property-info": "^6.4 || ^7.1", - "symfony/property-access": "^6.4 || ^7.1", - "symfony/serializer": "^6.4 || ^7.1", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/security-core": "^6.4 || ^7.0", - "willdurand/negotiation": "^3.0" + "willdurand/negotiation": "^3.1" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "api-platform/graphql": "^3.4 || ^4.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", + "phpspec/prophecy-phpunit": "^2.2", "symfony/routing": "^6.4 || ^7.0", + "phpunit/phpunit": "^11.2", "symfony/validator": "^6.4 || ^7.0", "symfony/mercure-bundle": "*", - "webonyx/graphql-php": "^14.0 || ^15.0", - "sebastian/comparator": "<5.0", + "webonyx/graphql-php": "^15.0", "api-platform/doctrine-common": "^3.4 || ^4.0", "api-platform/elasticsearch": "^3.4 || ^4.0", + "api-platform/graphql": "^3.4 || ^4.0", "api-platform/doctrine-orm": "^3.4 || ^4.0", "api-platform/doctrine-odm": "^3.4 || ^4.0", "api-platform/parameter-validator": "^3.1", - "symfony/expression-language": "^6.4 || ^7.1" + "symfony/expression-language": "^6.4 || ^7.0" }, "suggest": { "api-platform/doctrine-orm": "To support Doctrine ORM.", @@ -100,7 +105,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Util/ArrayTrait.php b/src/Util/ArrayTrait.php deleted file mode 100644 index bd69c4b2448..00000000000 --- a/src/Util/ArrayTrait.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -/** - * @deprecated use ApiPlatform\GraphQl\ArrayTrait - */ -trait ArrayTrait -{ - public function isSequentialArrayOfArrays(array $array): bool - { - if (!$this->isSequentialArray($array)) { - return false; - } - - return $this->arrayContainsOnly($array, 'array'); - } - - public function isSequentialArray(array $array): bool - { - if ([] === $array) { - return false; - } - - return array_is_list($array); - } - - public function arrayContainsOnly(array $array, string $type): bool - { - return $array === array_filter($array, static fn ($item): bool => $type === \gettype($item)); - } -} diff --git a/src/Util/AttributeFilterExtractorTrait.php b/src/Util/AttributeFilterExtractorTrait.php deleted file mode 100644 index 3ccbf4d0b3f..00000000000 --- a/src/Util/AttributeFilterExtractorTrait.php +++ /dev/null @@ -1,138 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use ApiPlatform\Metadata\ApiFilter; - -/** - * Generates a service id for a generic filter. - * - * @internal - * - * @author Antoine Bluchet - */ -trait AttributeFilterExtractorTrait -{ - /** - * Filters annotations to get back only ApiFilter annotations. - * - * @return \Iterator only ApiFilter annotations - */ - private function getFilterAttributes(\ReflectionClass|\ReflectionProperty $reflector): \Iterator - { - $attributes = $reflector->getAttributes(ApiFilter::class); - - foreach ($attributes as $attribute) { - yield $attribute->newInstance(); - } - } - - /** - * Given a filter attribute and reflection elements, find out the properties where the filter is applied. - */ - private function getFilterProperties(ApiFilter $filterAttribute, \ReflectionClass $reflectionClass, ?\ReflectionProperty $reflectionProperty = null): array - { - $properties = []; - - if ($filterAttribute->properties) { - foreach ($filterAttribute->properties as $property => $strategy) { - if (\is_int($property)) { - $properties[$strategy] = null; - } else { - $properties[$property] = $strategy; - } - } - - return $properties; - } - - if (null !== $reflectionProperty) { - $properties[$reflectionProperty->getName()] = $filterAttribute->strategy ?: null; - - return $properties; - } - - if ($filterAttribute->strategy) { - foreach ($reflectionClass->getProperties() as $reflectionProperty) { - $properties[$reflectionProperty->getName()] = $filterAttribute->strategy; - } - } - - return $properties; - } - - /** - * Reads filter attribute from a ReflectionClass. - * - * @return array Key is the filter id. It has two values, properties and the ApiFilter instance - */ - private function readFilterAttributes(\ReflectionClass $reflectionClass): array - { - $filters = []; - - foreach ($this->getFilterAttributes($reflectionClass) as $filterAttribute) { - $filterClass = $filterAttribute->filterClass; - $id = $this->generateFilterId($reflectionClass, $filterClass, $filterAttribute->id); - - if (!isset($filters[$id])) { - $filters[$id] = [$filterAttribute->arguments, $filterClass]; - } - - if ($properties = $this->getFilterProperties($filterAttribute, $reflectionClass)) { - $filters[$id][0]['properties'] = $properties; - } - } - - foreach ($reflectionClass->getProperties() as $reflectionProperty) { - foreach ($this->getFilterAttributes($reflectionProperty) as $filterAttribute) { - $filterClass = $filterAttribute->filterClass; - $id = $this->generateFilterId($reflectionClass, $filterClass, $filterAttribute->id); - - if (!isset($filters[$id])) { - $filters[$id] = [$filterAttribute->arguments, $filterClass]; - } - - if ($properties = $this->getFilterProperties($filterAttribute, $reflectionClass, $reflectionProperty)) { - if (isset($filters[$id][0]['properties'])) { - $filters[$id][0]['properties'] = array_merge($filters[$id][0]['properties'], $properties); - } else { - $filters[$id][0]['properties'] = $properties; - } - } - } - } - - $parent = $reflectionClass->getParentClass(); - - if (false !== $parent) { - return array_merge($filters, $this->readFilterAttributes($parent)); - } - - return $filters; - } - - /** - * Generates a unique, per-class and per-filter identifier prefixed by `annotated_`. - * - * @param \ReflectionClass $reflectionClass the reflection class of a Resource - * @param string $filterClass the filter class - * @param string|null $filterId the filter id - */ - private function generateFilterId(\ReflectionClass $reflectionClass, string $filterClass, ?string $filterId = null): string - { - $suffix = null !== $filterId ? '_'.$filterId : $filterId; - - return 'annotated_'.Inflector::tableize(str_replace('\\', '', $reflectionClass->getName().(new \ReflectionClass($filterClass))->getName().$suffix)); - } -} diff --git a/src/Util/AttributesExtractor.php b/src/Util/AttributesExtractor.php deleted file mode 100644 index e316c5e8023..00000000000 --- a/src/Util/AttributesExtractor.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -/** - * Extracts data used by the library form given attributes. - * - * @author Antoine Bluchet - * - * @internal - */ -final class AttributesExtractor -{ - private function __construct() - { - } - - /** - * Extracts resource class, operation name and format request attributes. Returns an empty array if the request does - * not contain required attributes. - */ - public static function extractAttributes(array $attributes): array - { - $result = ['resource_class' => $attributes['_api_resource_class'] ?? null, 'has_composite_identifier' => $attributes['_api_has_composite_identifier'] ?? false]; - - if (null === $result['resource_class']) { - return []; - } - - $hasRequestAttributeKey = false; - if (isset($attributes['_api_operation_name'])) { - $hasRequestAttributeKey = true; - $result['operation_name'] = $attributes['_api_operation_name']; - } - if (isset($attributes['_api_operation'])) { - $hasRequestAttributeKey = true; - $result['operation'] = $attributes['_api_operation']; - } - - if ($previousObject = $attributes['previous_data'] ?? null) { - $result['previous_data'] = $previousObject; - } - - if (false === $hasRequestAttributeKey) { - return []; - } - - $result += [ - 'receive' => (bool) ($attributes['_api_receive'] ?? true), - 'respond' => (bool) ($attributes['_api_respond'] ?? true), - 'persist' => (bool) ($attributes['_api_persist'] ?? true), - ]; - - return $result; - } -} diff --git a/src/Util/CachedTrait.php b/src/Util/CachedTrait.php deleted file mode 100644 index a1f71e091e9..00000000000 --- a/src/Util/CachedTrait.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use Psr\Cache\CacheException; -use Psr\Cache\CacheItemPoolInterface; - -/** - * @internal - */ -trait CachedTrait -{ - private CacheItemPoolInterface $cacheItemPool; - private array $localCache = []; - - private function getCached(string $cacheKey, callable $getValue): mixed - { - if (\array_key_exists($cacheKey, $this->localCache)) { - return $this->localCache[$cacheKey]; - } - - try { - $cacheItem = $this->cacheItemPool->getItem($cacheKey); - } catch (CacheException) { - return $this->localCache[$cacheKey] = $getValue(); - } - - if ($cacheItem->isHit()) { - return $this->localCache[$cacheKey] = $cacheItem->get(); - } - - $value = $getValue(); - - $cacheItem->set($value); - $this->cacheItemPool->save($cacheItem); - - return $this->localCache[$cacheKey] = $value; - } -} diff --git a/src/Util/CloneTrait.php b/src/Util/CloneTrait.php deleted file mode 100644 index 1944307ebe9..00000000000 --- a/src/Util/CloneTrait.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -/** - * Clones given data if cloneable. - * - * @internal - * - * @author Quentin Barloy - */ -trait CloneTrait -{ - public function clone(mixed $data): mixed - { - if (!\is_object($data)) { - return $data; - } - - try { - return (new \ReflectionClass($data))->isCloneable() ? clone $data : null; - } catch (\ReflectionException) { - return null; - } - } -} diff --git a/src/Util/ErrorFormatGuesser.php b/src/Util/ErrorFormatGuesser.php deleted file mode 100644 index f456a3e739a..00000000000 --- a/src/Util/ErrorFormatGuesser.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * Guesses the error format to use. - * - * @deprecated since API Platform 3.2 - * - * @author Kévin Dunglas - */ -final class ErrorFormatGuesser -{ - private function __construct() - { - } - - /** - * Get the error format and its associated MIME type. - */ - public static function guessErrorFormat(Request $request, array $errorFormats): array - { - $requestFormat = $request->getRequestFormat(''); - - if ('' !== $requestFormat && isset($errorFormats[$requestFormat])) { - return ['key' => $requestFormat, 'value' => $errorFormats[$requestFormat]]; - } - - $requestMimeTypes = Request::getMimeTypes($request->getRequestFormat()); - $defaultFormat = []; - - foreach ($errorFormats as $format => $errorMimeTypes) { - if (array_intersect($requestMimeTypes, $errorMimeTypes)) { - return ['key' => $format, 'value' => $errorMimeTypes]; - } - - if (!$defaultFormat) { - $defaultFormat = ['key' => $format, 'value' => $errorMimeTypes]; - } - } - - return $defaultFormat; - } -} diff --git a/src/Util/Inflector.php b/src/Util/Inflector.php deleted file mode 100644 index 5dc86a07bb8..00000000000 --- a/src/Util/Inflector.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use Doctrine\Inflector\Inflector as LegacyInflector; -use Doctrine\Inflector\InflectorFactory; - -/** - * @internal - */ -final class Inflector -{ - private static ?LegacyInflector $instance = null; - - private static function getInstance(): LegacyInflector - { - return self::$instance - ?? self::$instance = InflectorFactory::create()->build(); - } - - /** - * @see InflectorObject::tableize() - */ - public static function tableize(string $word): string - { - return self::getInstance()->tableize($word); - } - - /** - * @see InflectorObject::pluralize() - */ - public static function pluralize(string $word): string - { - return self::getInstance()->pluralize($word); - } -} diff --git a/src/Util/IriHelper.php b/src/Util/IriHelper.php deleted file mode 100644 index cf2fb7168a4..00000000000 --- a/src/Util/IriHelper.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\Util\RequestParser; - -/** - * Parses and creates IRIs. - * - * @author Kévin Dunglas - * - * @deprecated use ApiPlatform\Metadata\Util\IriHelper instead - * - * @internal - */ -final class IriHelper -{ - private function __construct() - { - } - - /** - * Parses and standardizes the request IRI. - * - * @throws InvalidArgumentException - */ - public static function parseIri(string $iri, string $pageParameterName): array - { - $parts = parse_url($iri); - if (false === $parts) { - throw new InvalidArgumentException(\sprintf('The request URI "%s" is malformed.', $iri)); - } - - $parameters = []; - if (isset($parts['query'])) { - $parameters = RequestParser::parseRequestParams($parts['query']); - - // Remove existing page parameter - unset($parameters[$pageParameterName]); - } - - return ['parts' => $parts, 'parameters' => $parameters]; - } - - /** - * Gets a collection IRI for the given parameters. - */ - public static function createIri(array $parts, array $parameters, ?string $pageParameterName = null, ?float $page = null, $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): string - { - if (null !== $page && null !== $pageParameterName) { - $parameters[$pageParameterName] = $page; - } - - $query = http_build_query($parameters, '', '&', \PHP_QUERY_RFC3986); - $parts['query'] = preg_replace('/%5B\d+%5D/', '%5B%5D', $query); - - $url = ''; - if ((UrlGeneratorInterface::ABS_URL === $urlGenerationStrategy || UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy) && isset($parts['host'])) { - if (isset($parts['scheme'])) { - $scheme = $parts['scheme']; - } elseif (isset($parts['port']) && 443 === $parts['port']) { - $scheme = 'https'; - } else { - $scheme = 'http'; - } - $url .= UrlGeneratorInterface::NET_PATH === $urlGenerationStrategy ? '//' : "$scheme://"; - - if (isset($parts['user'])) { - $url .= $parts['user']; - - if (isset($parts['pass'])) { - $url .= ':'.$parts['pass']; - } - - $url .= '@'; - } - - $url .= $parts['host']; - - if (isset($parts['port'])) { - $url .= ':'.$parts['port']; - } - } - - $url .= $parts['path']; - - if ('' !== $parts['query']) { - $url .= '?'.$parts['query']; - } - - if (isset($parts['fragment'])) { - $url .= '#'.$parts['fragment']; - } - - return $url; - } -} diff --git a/src/Util/Reflection.php b/src/Util/Reflection.php deleted file mode 100644 index 97a6880b41a..00000000000 --- a/src/Util/Reflection.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -/** - * Reflection utilities. - * - * @internal - * - * @author Kévin Dunglas - */ -final class Reflection -{ - public const ACCESSOR_PREFIXES = ['get', 'is', 'has', 'can']; - public const MUTATOR_PREFIXES = ['set', 'add', 'remove']; - - /** - * Gets the property name associated with an accessor method. - */ - public function getProperty(string $methodName): ?string - { - $pattern = implode('|', array_merge(self::ACCESSOR_PREFIXES, self::MUTATOR_PREFIXES)); - - if (preg_match('/^('.$pattern.')(.+)$/i', $methodName, $matches)) { - return $matches[2]; - } - - return null; - } -} diff --git a/src/Util/RequestAttributesExtractor.php b/src/Util/RequestAttributesExtractor.php deleted file mode 100644 index 64cd19d4c84..00000000000 --- a/src/Util/RequestAttributesExtractor.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * Extracts data used by the library form a Request instance. - * - * @deprecated use \ApiPlatform\State\Util\RequestAttributesExtractor - * - * @author Kévin Dunglas - */ -final class RequestAttributesExtractor -{ - private function __construct() - { - } - - /** - * Extracts resource class, operation name and format request attributes. Returns an empty array if the request does - * not contain required attributes. - */ - public static function extractAttributes(Request $request): array - { - return AttributesExtractor::extractAttributes($request->attributes->all()); - } -} diff --git a/src/Util/SortTrait.php b/src/Util/SortTrait.php deleted file mode 100644 index 04a8a31917c..00000000000 --- a/src/Util/SortTrait.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Util; - -/** - * Sort helper methods. - * - * @internal - * - * @author Alan Poulain - */ -trait SortTrait -{ - private function arrayRecursiveSort(array &$array, callable $sortFunction): void - { - foreach ($array as &$value) { - if (\is_array($value)) { - $this->arrayRecursiveSort($value, $sortFunction); - } - } - unset($value); - $sortFunction($array); - } -} diff --git a/src/Validator/Exception/ValidationException.php b/src/Validator/Exception/ValidationException.php index 720f1f26ca1..ef38c3d7ec8 100644 --- a/src/Validator/Exception/ValidationException.php +++ b/src/Validator/Exception/ValidationException.php @@ -43,7 +43,6 @@ outputFormats: ['json' => ['application/problem+json']], normalizationContext: ['groups' => ['json'], 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, ]), new ErrorOperation( name: '_api_validation_errors_hydra', @@ -52,13 +51,12 @@ normalizationContext: [ 'groups' => ['jsonld'], 'skip_null_values' => true, - 'rfc_7807_compliant_errors' => true, ] ), new ErrorOperation( name: '_api_validation_errors_jsonapi', outputFormats: ['jsonapi' => ['application/vnd.api+json']], - normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true, 'rfc_7807_compliant_errors' => true] + normalizationContext: ['groups' => ['jsonapi'], 'skip_null_values' => true] ), ], graphQlOperations: [] @@ -114,19 +112,19 @@ public function getDescription(): string return $this->detail; } - #[Groups(['jsonld', 'json'])] + #[Groups(['jsonld', 'json', 'jsonapi'])] public function getType(): string { return '/validation_errors/'.$this->getId(); } - #[Groups(['jsonld', 'json'])] + #[Groups(['jsonld', 'json', 'jsonapi'])] public function getTitle(): ?string { return $this->errorTitle ?? 'An error occurred'; } - #[Groups(['jsonld', 'json'])] + #[Groups(['jsonld', 'json', 'jsonapi'])] private string $detail; public function getDetail(): ?string @@ -139,7 +137,7 @@ public function setDetail(string $detail): void $this->detail = $detail; } - #[Groups(['jsonld', 'json'])] + #[Groups(['jsonld', 'json', 'jsonapi'])] public function getStatus(): ?int { return $this->status; @@ -150,7 +148,7 @@ public function setStatus(int $status): void $this->status = $status; } - #[Groups(['jsonld', 'json'])] + #[Groups(['jsonld', 'json', 'jsonapi'])] public function getInstance(): ?string { return null; diff --git a/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..e34124f08ec --- /dev/null +++ b/src/Validator/Metadata/Resource/Factory/ParameterValidationResourceMetadataCollectionFactory.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Validator\Metadata\Resource\Factory; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Psr\Container\ContainerInterface; +use Symfony\Component\Validator\Constraints\Choice; +use Symfony\Component\Validator\Constraints\Count; +use Symfony\Component\Validator\Constraints\DivisibleBy; +use Symfony\Component\Validator\Constraints\GreaterThan; +use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\LessThan; +use Symfony\Component\Validator\Constraints\LessThanOrEqual; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Constraints\Type; +use Symfony\Component\Validator\Constraints\Unique; + +final class ParameterValidationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + private readonly ?ContainerInterface $filterLocator = null, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $i => $resource) { + $operations = $resource->getOperations(); + + foreach ($operations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($parameters as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + // As we deprecate the parameter validator, we declare a parameter for each filter transfering validation to the new system + if ($operation->getFilters() && 0 === $parameters->count()) { + $parameters = $this->addFilterValidation($operation); + } + + if (\count($parameters) > 0) { + $operations->add($operationName, $operation->withParameters($parameters)); + } + } + + $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort()); + + if (!$graphQlOperations = $resource->getGraphQlOperations()) { + continue; + } + + foreach ($graphQlOperations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($operation->getParameters() ?? [] as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + if (\count($parameters) > 0) { + $graphQlOperations[$operationName] = $operation->withParameters($parameters); + } + } + + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + } + + return $resourceMetadataCollection; + } + + private function addSchemaValidation(Parameter $parameter, ?array $schema = null, ?bool $required = null, ?OpenApiParameter $openApi = null): Parameter + { + if (null !== $parameter->getConstraints()) { + return $parameter; + } + + $schema ??= $parameter->getSchema(); + $required ??= $parameter->getRequired() ?? false; + $openApi ??= $parameter->getOpenApi(); + + // When it's an array of openapi parameters take the first one as it's probably just a variant of the query parameter, + // only getAllowEmptyValue is used here anyways + if (\is_array($openApi)) { + $openApi = $openApi[0]; + } elseif (false === $openApi) { + $openApi = null; + } + + $assertions = []; + + if ($required && false !== ($allowEmptyValue = $openApi?->getAllowEmptyValue())) { + $assertions[] = new NotNull(message: \sprintf('The parameter "%s" is required.', $parameter->getKey())); + } + + if (false === ($allowEmptyValue ?? $openApi?->getAllowEmptyValue())) { + $assertions[] = new NotBlank(allowNull: !$required); + } + + if (isset($schema['exclusiveMinimum'])) { + $assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']); + } + + if (isset($schema['exclusiveMaximum'])) { + $assertions[] = new LessThan(value: $schema['exclusiveMaximum']); + } + + if (isset($schema['minimum'])) { + $assertions[] = new GreaterThanOrEqual(value: $schema['minimum']); + } + + if (isset($schema['maximum'])) { + $assertions[] = new LessThanOrEqual(value: $schema['maximum']); + } + + if (isset($schema['pattern'])) { + $assertions[] = new Regex('#'.$schema['pattern'].'#'); + } + + if (isset($schema['maxLength']) || isset($schema['minLength'])) { + $assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null); + } + + if (isset($schema['minItems']) || isset($schema['maxItems'])) { + $assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null); + } + + if (isset($schema['multipleOf'])) { + $assertions[] = new DivisibleBy(value: $schema['multipleOf']); + } + + if ($schema['uniqueItems'] ?? false) { + $assertions[] = new Unique(); + } + + if (isset($schema['enum'])) { + $assertions[] = new Choice(choices: $schema['enum']); + } + + if (isset($schema['type']) && 'array' === $schema['type']) { + $assertions[] = new Type(type: 'array'); + } + + if (!$assertions) { + return $parameter; + } + + if (1 === \count($assertions)) { + return $parameter->withConstraints($assertions[0]); + } + + return $parameter->withConstraints($assertions); + } + + private function addFilterValidation(HttpOperation $operation): Parameters + { + $parameters = new Parameters(); + $internalPriority = -1; + + foreach ($operation->getFilters() as $filter) { + if (!$this->filterLocator->has($filter)) { + continue; + } + + $filter = $this->filterLocator->get($filter); + foreach ($filter->getDescription($operation->getClass()) as $parameterName => $definition) { + $key = $parameterName; + $required = $definition['required'] ?? false; + $schema = $definition['schema'] ?? null; + + $openApi = null; + if (isset($definition['openapi']) && $definition['openapi'] instanceof OpenApiParameter) { + $openApi = $definition['openapi']; + } + + // The query parameter validator forced this, lets maintain BC on filters + if (true === $required && !$openApi) { + $openApi = new OpenApiParameter(name: $key, in: 'query', allowEmptyValue: false); + } + + $parameters->add($key, $this->addSchemaValidation( + // we disable openapi and hydra on purpose as their docs comes from filters see the condition for addFilterValidation above + new QueryParameter(key: $key, property: $definition['property'] ?? null, priority: $internalPriority--, schema: $schema, openApi: false, hydra: false), + $schema, + $required, + $openApi + )); + } + } + + return $parameters; + } +} diff --git a/src/Validator/composer.json b/src/Validator/composer.json index 2bd1cd748d2..a09c071ff18 100644 --- a/src/Validator/composer.json +++ b/src/Validator/composer.json @@ -22,15 +22,14 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "api-platform/metadata": "^3.4 || ^4.0", - "symfony/web-link": "^6.4 || ^7.1" + "symfony/web-link": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", + "phpspec/prophecy-phpunit": "^2.2", "symfony/serializer": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0", + "phpunit/phpunit": "^11.2", "symfony/validator": "^6.4 || ^7.0", "symfony/http-kernel": "^6.4 || ^7.0" }, @@ -55,7 +54,7 @@ "dev-3.4": "3.4.x-dev" }, "symfony": { - "require": "^6.4 || ^7.1" + "require": "^6.4 || ^7.0" }, "thanks": { "name": "api-platform/api-platform", diff --git a/src/Validator/phpunit.xml.dist b/src/Validator/phpunit.xml.dist index 6774dbf7dbc..cdad2003429 100644 --- a/src/Validator/phpunit.xml.dist +++ b/src/Validator/phpunit.xml.dist @@ -1,30 +1,20 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + ./ + + + ./Tests + ./vendor + + diff --git a/src/deprecation.php b/src/deprecation.php deleted file mode 100644 index e6ab7eab89a..00000000000 --- a/src/deprecation.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -$deprecatedClassesWithAliases = [ - ApiPlatform\HttpCache\EventListener\AddHeadersListener::class => ApiPlatform\Symfony\EventListener\AddHeadersListener::class, - ApiPlatform\HttpCache\EventListener\AddTagsListener::class => ApiPlatform\Symfony\EventListener\AddTagsListener::class, - ApiPlatform\Exception\FilterValidationException::class => ApiPlatform\ParameterValidator\Exception\ValidationException::class, - ApiPlatform\Api\QueryParameterValidator\Validator\ArrayItems::class => ApiPlatform\ParameterValidator\Validator\ArrayItems::class, - ApiPlatform\Api\QueryParameterValidator\Validator\Bounds::class => ApiPlatform\ParameterValidator\Validator\Bounds::class, - ApiPlatform\Api\QueryParameterValidator\Validator\Enum::class => ApiPlatform\ParameterValidator\Validator\Enum::class, - ApiPlatform\Api\QueryParameterValidator\Validator\Length::class => ApiPlatform\ParameterValidator\Validator\Length::class, - ApiPlatform\Api\QueryParameterValidator\Validator\MultipleOf::class => ApiPlatform\ParameterValidator\Validator\MultipleOf::class, - ApiPlatform\Api\QueryParameterValidator\Validator\Pattern::class => ApiPlatform\ParameterValidator\Validator\Pattern::class, - ApiPlatform\Api\QueryParameterValidator\Validator\Required::class => ApiPlatform\ParameterValidator\Validator\Required::class, -]; - -$movedClasses = [ - ApiPlatform\Action\EntrypointAction::class => ApiPlatform\Symfony\Action\EntrypointAction::class, - ApiPlatform\Action\NotExposedAction::class => ApiPlatform\Symfony\Action\NotExposedAction::class, - ApiPlatform\Action\NotFoundAction::class => ApiPlatform\Symfony\Action\NotFoundAction::class, - ApiPlatform\Action\PlaceholderAction::class => ApiPlatform\Symfony\Action\PlaceholderAction::class, - ApiPlatform\Api\Entrypoint::class => ApiPlatform\Documentation\Entrypoint::class, - ApiPlatform\Api\UrlGeneratorInterface::class => ApiPlatform\Metadata\UrlGeneratorInterface::class, - ApiPlatform\Exception\ExceptionInterface::class => ApiPlatform\Metadata\Exception\ExceptionInterface::class, - ApiPlatform\Exception\InvalidArgumentException::class => ApiPlatform\Metadata\Exception\InvalidArgumentException::class, - ApiPlatform\Exception\InvalidIdentifierException::class => ApiPlatform\Metadata\Exception\InvalidIdentifierException::class, - ApiPlatform\Exception\InvalidUriVariableException::class => ApiPlatform\Metadata\Exception\InvalidUriVariableException::class, - ApiPlatform\Exception\ItemNotFoundException::class => ApiPlatform\Metadata\Exception\ItemNotFoundException::class, - ApiPlatform\Exception\NotExposedHttpException::class => ApiPlatform\Metadata\Exception\NotExposedHttpException::class, - ApiPlatform\Exception\OperationNotFoundException::class => ApiPlatform\Metadata\Exception\OperationNotFoundException::class, - ApiPlatform\Exception\PropertyNotFoundException::class => ApiPlatform\Metadata\Exception\PropertyNotFoundException::class, - ApiPlatform\Exception\ResourceClassNotFoundException::class => ApiPlatform\Metadata\Exception\ResourceClassNotFoundException::class, - ApiPlatform\Exception\RuntimeException::class => ApiPlatform\Metadata\Exception\RuntimeException::class, - ApiPlatform\GraphQl\Type\TypeBuilderInterface::class => ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface::class, - ApiPlatform\GraphQl\Type\TypeBuilderEnumInterface::class => ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface::class, - ApiPlatform\Operation\DashPathSegmentNameGenerator::class => ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator::class, - ApiPlatform\Operation\UnderscorePathSegmentNameGenerator::class => ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator::class, - ApiPlatform\Operation\PathSegmentNameGeneratorInterface::class => ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface::class, - ApiPlatform\Symfony\Bundle\Command\OpenApiCommand::class => ApiPlatform\OpenApi\Command\OpenApiCommand::class, - ApiPlatform\Util\ClientTrait::class => ApiPlatform\Symfony\Bundle\Test\ClientTrait::class, - ApiPlatform\Util\RequestAttributesExtractor::class => ApiPlatform\State\Util\RequestAttributesExtractor::class, - ApiPlatform\Symfony\Util\RequestAttributesExtractor::class => ApiPlatform\State\Util\RequestAttributesExtractor::class, - ApiPlatform\Doctrine\EventListener\PublishMercureUpdatesListener::class => ApiPlatform\Symfony\Doctrine\EventListener\PublishMercureUpdatesListener::class, - ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener::class => ApiPlatform\Symfony\Doctrine\EventListener\PurgeHttpCacheListener::class, -]; - -$removedClasses = [ - ApiPlatform\Action\ExceptionAction::class => true, - ApiPlatform\Exception\DeserializationException::class => true, - ApiPlatform\Exception\ErrorCodeSerializableInterface::class => true, - ApiPlatform\Exception\FilterValidationException::class => true, - ApiPlatform\Exception\InvalidResourceException::class => true, - ApiPlatform\Exception\InvalidValueException::class => true, - ApiPlatform\Exception\ResourceClassNotSupportedException::class => true, - ApiPlatform\GraphQl\Resolver\Factory\CollectionResolverFactory::class => true, - ApiPlatform\GraphQl\Resolver\Factory\ItemMutationResolverFactory::class => true, - ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory::class => true, - ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory::class => true, - ApiPlatform\GraphQl\Resolver\Stage\DeserializeStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\ReadStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SerializeStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\ValidateStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface::class => true, - ApiPlatform\GraphQl\Resolver\Stage\WriteStage::class => true, - ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface::class => true, - ApiPlatform\HttpCache\EventListener\AddHeadersListener::class => true, - ApiPlatform\HttpCache\EventListener\AddTagsListener::class => true, - ApiPlatform\Hydra\EventListener\AddLinkHeaderListener::class => true, - ApiPlatform\Hydra\Serializer\ErrorNormalizer::class => true, - ApiPlatform\JsonSchema\TypeFactory::class => true, - ApiPlatform\JsonSchema\TypeFactoryInterface::class => true, - ApiPlatform\Problem\Serializer\ErrorNormalizer::class => true, - ApiPlatform\Serializer\CacheableSupportsMethodInterface::class => true, - ApiPlatform\OpenApi\Serializer\CacheableSupportsMethodInterface::class => true, - ApiPlatform\Symfony\EventListener\AddHeadersListener::class => true, - ApiPlatform\Symfony\EventListener\AddLinkHeaderListener::class => true, - ApiPlatform\Symfony\EventListener\AddTagsListener::class => true, - ApiPlatform\Symfony\EventListener\DenyAccessListener::class => true, - ApiPlatform\Symfony\EventListener\QueryParameterValidateListener::class => true, - ApiPlatform\Symfony\Validator\EventListener\ValidationExceptionListener::class => true, - ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface::class => true, - ApiPlatform\Symfony\Validator\Exception\ValidationException::class => true, - ApiPlatform\Symfony\Validator\State\QueryParameterValidateProvider::class => true, - ApiPlatform\Util\ErrorFormatGuesser::class => true, -]; - -spl_autoload_register(function ($className) use ($deprecatedClassesWithAliases, $movedClasses, $removedClasses): void { - if (isset($removedClasses[$className])) { - trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated and will be removed.', $className)); - - return; - } - - if (isset($movedClasses[$className])) { - trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated, use %s instead.', $className, $movedClasses[$className])); - - return; - } - - if (isset($deprecatedClassesWithAliases[$className])) { - trigger_deprecation('api-platform/core', '4.0', sprintf('The class %s is deprecated, use %s instead.', $className, $deprecatedClassesWithAliases[$className])); - - class_alias($deprecatedClassesWithAliases[$className], $className); - - return; - } -}); diff --git a/tests/.ignored-deprecations b/tests/.ignored-deprecations deleted file mode 100644 index 960c69a2c94..00000000000 --- a/tests/.ignored-deprecations +++ /dev/null @@ -1,31 +0,0 @@ -# No fix available yet, see https://github.com/doctrine/dbal/issues/5784 -%Subscribing to onSchemaCreateTable events is deprecated\.% - -# Fixed by https://github.com/doctrine/orm/pull/10855 -%Column::setCustomSchemaOptions\(\) is deprecated\. Use setPlatformOptions\(\) instead\.% - -# Fixed in DoctrineMongoDBBundle 4.6 -%Accessing Doctrine\\Common\\Lexer\\Token properties via ArrayAccess is deprecated, use the value, type or position property instead% -%Do the same.*Doctrine\\Bundle\\MongoDBBundle% - -# Fixed when ApiPlatform\Api\FilterLocatorTrait will we deleted -%ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% - -%The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% - -%Since symfony/validator 7.1: Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".% - -%$fieldsBuilder argument of SchemaBuilder implementing "ApiPlatform\\GraphQl\\Type\\FieldsBuilderInterface" is deprecated since API Platform 3.1. It has to implement "ApiPlatform\\GraphQl\\Type\\FieldsBuilderEnumInterface" instead.% - -# waiting for tag https://github.com/doctrine/DoctrineBundle/commit/d84aadb257bb8dc278106c0e53fffe2babcdcd8e -%Since symfony/dependency-injection 7.2: Type "tagged" is deprecated for tag , use "tagged_iterator" instead% - -# old test filters will be removed in 4.0 -%Since api-platform/core 3.4: The key "swagger" in a filter description is deprecated, use "schema" or "openapi" instead.% -%Since api-platform/core 3.4: The key "openapi" in a filter description should be a "ApiPlatform\\OpenApi\\Model\\Parameter" class or use "schema" to specify the JSON Schema.% -%Since api-platform/core 3.4: The "exclusiveMaximum" schema value should be a number not a boolean.% -%Since api-platform/core 3.4: The "exclusiveMinimum" schema value should be a number not a boolean.% -%Since api-platform/core 3.4: The "allowEmptyValue" option should be declared using an "openapi" parameter.% -%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\JsonSchema\\SchemaFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% -%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\OpenApi\\Factory\\OpenApiFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% -%Since api-platform/core 3.3: Use a "ApiPlatform\\State\\ProviderInterface" as first argument in "ApiPlatform\\Symfony\\EventListener\\QueryParameterValidateListener" instead of "ApiPlatform\\ParameterValidator\\ParameterValidator".% diff --git a/tests/.ignored-deprecations-legacy-events b/tests/.ignored-deprecations-legacy-events deleted file mode 100644 index e9cd0f40746..00000000000 --- a/tests/.ignored-deprecations-legacy-events +++ /dev/null @@ -1,30 +0,0 @@ -# No fix available yet, see https://github.com/doctrine/dbal/issues/5784 -%Subscribing to onSchemaCreateTable events is deprecated\.% - -# Fixed by https://github.com/doctrine/orm/pull/10855 -%Column::setCustomSchemaOptions\(\) is deprecated\. Use setPlatformOptions\(\) instead\.% - -# Fixed in DoctrineMongoDBBundle 4.6 -%Accessing Doctrine\\Common\\Lexer\\Token properties via ArrayAccess is deprecated, use the value, type or position property instead% -%Do the same.*Doctrine\\Bundle\\MongoDBBundle% - -# These are expected for the events legacy layer -%Since api-platform/core 3.3: The "event_listeners_backward_compatibility_layer" will be removed in 4.0. Use the configuration "use_symfony_listeners" to use Symfony listeners. The following listeners are deprecated and will be removed in API Platform 4.0: "ApiPlatform\\Symfony\\EventListener\\AddHeadersListener, ApiPlatform\\Symfony\\EventListener\\AddTagsListener, ApiPlatform\\Symfony\\EventListener\\AddLinkHeaderListener, ApiPlatform\\Hydra\\EventListener\\AddLinkHeaderListener, ApiPlatform\\Symfony\\EventListener\\DenyAccessListener"% - -%Since api-platform/core 3.3: Use a "ApiPlatform\\State\\ProviderInterface" as first argument in "ApiPlatform\\Symfony\\EventListener\\DeserializeListener" instead of "Symfony\\Component\\Serializer\\SerializerInterface".% - -%Since api-platform/core 3.3: Use a "ApiPlatform\\State\\Provider\\ReadProvider" as first argument in "ApiPlatform\\Symfony\\EventListener\\ReadListener" instead of "ApiPlatform\\State\\CallableProvider".% - -%Since api-platform/core 3.3: Use a "ApiPlatform\\State\\ProviderInterface" as first argument in "ApiPlatform\\Symfony\\EventListener\\QueryParameterValidateListener" instead of "ApiPlatform\\ParameterValidator\\ParameterValidator".% - -%Since api-platform/core 3.3: Use a "ApiPlatform\\State\\ProviderInterface" as first argument in "ApiPlatform\\Symfony\\EventListener\\AddFormatListener" instead of "Negotiation\\Negotiator".% - -%Since api-platform/core 3.3: Use a "ApiPlatform\\Metadata\\Resource\\Factory\\ResourceMetadataCollectionFactoryInterface" as second argument in "ApiPlatform\\Symfony\\EventListener\\DeserializeListener" instead of "ApiPlatform\\State\\SerializerContextBuilderInterface".% - -# Fixed when ApiPlatform\Api\FilterLocatorTrait will we deleted -%ApiPlatform\\Api\\FilterInterface is deprecated in favor of ApiPlatform\\Metadata\\FilterInterface% - -%The "Symfony\\Bundle\\MakerBundle\\Maker\\MakeAuthenticator" class is deprecated, use any of the Security\\Make\* commands instead% -%Since symfony/validator 7.1: Not passing a value for the "requireTld" option to the Url constraint is deprecated. Its default value will change to "true".% -%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\JsonSchema\\SchemaFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% -%Since api-platform/core 3.4: Injecting the "ApiPlatform\\JsonSchema\\TypeFactoryInterface" inside "ApiPlatform\\OpenApi\\Factory\\OpenApiFactory" is deprecated and "ApiPlatform\\JsonSchema\\TypeFactoryInterface" will be removed in 4.x.% diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php deleted file mode 100644 index 4de8b47204c..00000000000 --- a/tests/Action/ExceptionActionTest.php +++ /dev/null @@ -1,245 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Action; - -use ApiPlatform\Action\ExceptionAction; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Serializer\Exception\ExceptionInterface; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * @author Amrouche Hamza - * @author Baptiste Meyer - * - * @group time-sensitive - */ -class ExceptionActionTest extends TestCase -{ - use ExpectDeprecationTrait; - use ProphecyTrait; - - public function testActionWithCatchableException(): void - { - $serializerException = $this->prophesize(ExceptionInterface::class); - if (!is_a(ExceptionInterface::class, \Throwable::class, true)) { - $serializerException->willExtend(\Exception::class); - } - $flattenException = FlattenException::create($serializerException->reveal()); // @phpstan-ignore-line - $serializer = $this->prophesize(SerializerInterface::class); - $serializer->serialize($flattenException, 'jsonproblem', ['statusCode' => Response::HTTP_BAD_REQUEST, 'rfc_7807_compliant_errors' => false])->willReturn(''); - - $exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']], [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST]); - - $request = new Request(); - $request->setFormat('jsonproblem', 'application/problem+json'); - - $response = $exceptionAction($flattenException, $request); - $this->assertSame('', $response->getContent()); - $this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode()); - $this->assertTrue($response->headers->contains('Content-Type', 'application/problem+json; charset=utf-8')); - $this->assertTrue($response->headers->contains('X-Content-Type-Options', 'nosniff')); - $this->assertTrue($response->headers->contains('X-Frame-Options', 'deny')); - } - - /** - * @dataProvider provideOperationExceptionToStatusCases - */ - public function testActionWithOperationExceptionToStatus( - array $globalExceptionToStatus, - ?array $resourceExceptionToStatus, - ?array $operationExceptionToStatus, - int $expectedStatusCode, - ): void { - $exception = new \DomainException(); - $flattenException = FlattenException::create($exception); - - $serializer = $this->prophesize(SerializerInterface::class); - $serializer->serialize($flattenException, 'jsonproblem', ['statusCode' => $expectedStatusCode, 'rfc_7807_compliant_errors' => false])->willReturn(''); - - /** @var HttpOperation $operation */ - $operation = (new Get())->withShortName('Foo'); - $resource = (new ApiResource())->withShortName('Foo'); - if ($resourceExceptionToStatus) { - $resource = $resource->withExceptionToStatus($resourceExceptionToStatus); - $operation = $operation->withExceptionToStatus($resourceExceptionToStatus); - } - - if ($operationExceptionToStatus) { - $operation = $operation->withExceptionToStatus($operationExceptionToStatus); - } - - $resource = $resource->withOperations(new Operations(['operation' => $operation])); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ - $resource, - ])); - - $exceptionAction = new ExceptionAction( - $serializer->reveal(), - [ - 'jsonproblem' => ['application/problem+json'], - 'jsonld' => ['application/ld+json'], - ], - $globalExceptionToStatus, - $resourceMetadataFactory->reveal() - ); - - $request = new Request(); - $request->setFormat('jsonproblem', 'application/problem+json'); - $request->attributes->replace([ - '_api_resource_class' => 'Foo', - '_api_operation_name' => 'operation', - ]); - - $response = $exceptionAction($flattenException, $request); - - $this->assertSame('', $response->getContent()); - $this->assertSame($expectedStatusCode, $response->getStatusCode()); - $this->assertTrue($response->headers->contains('Content-Type', 'application/problem+json; charset=utf-8')); - $this->assertTrue($response->headers->contains('X-Content-Type-Options', 'nosniff')); - $this->assertTrue($response->headers->contains('X-Frame-Options', 'deny')); - } - - public static function provideOperationExceptionToStatusCases(): \Generator - { - yield 'no mapping' => [ - [], - null, - null, - 500, - ]; - - yield 'on global attributes' => [ - [\DomainException::class => 100], - null, - null, - 100, - ]; - - yield 'on global attributes with empty resource and operation attributes' => [ - [\DomainException::class => 100], - [], - [], - 100, - ]; - - yield 'on global attributes and resource attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], - null, - 200, - ]; - - yield 'on global attributes and resource attributes with empty operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], - [], - 200, - ]; - - yield 'on global attributes and operation attributes' => [ - [\DomainException::class => 100], - null, - [\DomainException::class => 300], - 300, - ]; - - yield 'on global attributes and operation attributes with empty resource attributes' => [ - [\DomainException::class => 100], - [], - [\DomainException::class => 300], - 300, - ]; - - yield 'on global, resource and operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], - [\DomainException::class => 300], - 300, - ]; - - yield 'on resource attributes' => [ - [], - [\DomainException::class => 200], - null, - 200, - ]; - - yield 'on resource attributes with empty operation attributes' => [ - [], - [\DomainException::class => 200], - [], - 200, - ]; - - yield 'on resource and operation attributes' => [ - [], - [\DomainException::class => 200], - [\DomainException::class => 300], - 300, - ]; - - yield 'on operation attributes' => [ - [], - null, - [\DomainException::class => 300], - 300, - ]; - - yield 'on operation attributes with empty resource attributes' => [ - [], - [], - [\DomainException::class => 300], - 300, - ]; - } - - public function testActionWithUncatchableException(): void - { - $serializerException = $this->prophesize(ExceptionInterface::class); - if (!is_a(ExceptionInterface::class, \Throwable::class, true)) { - $serializerException->willExtend(\Exception::class); - } - - $flattenException = class_exists(FlattenException::class) ? FlattenException::create($serializerException->reveal()) : LegacyFlattenException::create($serializerException->reveal()); /** @phpstan-ignore-line */ - $serializer = $this->prophesize(SerializerInterface::class); - $serializer->serialize($flattenException, 'jsonproblem', ['statusCode' => $flattenException->getStatusCode(), 'rfc_7807_compliant_errors' => false])->willReturn(''); - - $exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]); - - $request = new Request(); - $request->setFormat('jsonproblem', 'application/problem+json'); - - $expected = new Response('', Response::HTTP_INTERNAL_SERVER_ERROR, [ - 'Content-Type' => 'application/problem+json; charset=utf-8', - 'X-Content-Type-Options' => 'nosniff', - 'X-Frame-Options' => 'deny', - ]); - - $this->assertEquals($expected, $exceptionAction($flattenException, $request)); - } -} diff --git a/tests/Behat/CoverageContext.php b/tests/Behat/CoverageContext.php index ffb4e6e3f11..ee5c171cd3d 100644 --- a/tests/Behat/CoverageContext.php +++ b/tests/Behat/CoverageContext.php @@ -19,6 +19,7 @@ use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Filter; use SebastianBergmann\CodeCoverage\Report\PHP; +use Symfony\Component\Finder\Finder; /** * Behat coverage. @@ -40,15 +41,28 @@ final class CoverageContext implements Context public static function setup(): void { $filter = new Filter(); - if (method_exists($filter, 'includeDirectory')) { - $filter->includeDirectory(__DIR__.'/../../src'); - self::$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter); + $finder = + (new Finder()) + ->in(__DIR__.'/../../src') + ->exclude([ + 'src/Core/Bridge/Symfony/Maker/Resources/skeleton', + 'tests/Fixtures/app/var', + 'docs/guides', + 'docs/var', + 'src/Doctrine/Orm/Tests/var', + 'src/Doctrine/Odm/Tests/var', + ]) + ->append([ + 'tests/Fixtures/app/console', + ]) + ->files() + ->name('*.php'); - return; + foreach ($finder as $file) { + $filter->includeFile((string) $file); } - $filter->addDirectoryToWhitelist(__DIR__.'/../../src'); // @phpstan-ignore-line - self::$coverage = new CodeCoverage(null, $filter); // @phpstan-ignore-line + self::$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter); } /** diff --git a/tests/Behat/ElasticsearchContext.php b/tests/Behat/ElasticsearchContext.php index 9bcc24fe4ee..37737cd562f 100644 --- a/tests/Behat/ElasticsearchContext.php +++ b/tests/Behat/ElasticsearchContext.php @@ -15,7 +15,6 @@ use Behat\Behat\Context\Context; use Elastic\Elasticsearch\Client; -use Elasticsearch\Client as LegacyClient; use Symfony\Component\Finder\Finder; /** @@ -25,7 +24,7 @@ */ final class ElasticsearchContext implements Context { - public function __construct(private readonly LegacyClient|Client $client, private readonly string $elasticsearchMappingsPath, private readonly string $elasticsearchFixturesPath) // @phpstan-ignore-line + public function __construct(private readonly Client $client, private readonly string $elasticsearchMappingsPath, private readonly string $elasticsearchFixturesPath) { } @@ -77,7 +76,6 @@ private function createIndexesAndMappings(): void $finder->files()->in($this->elasticsearchMappingsPath); foreach ($finder as $file) { - // @phpstan-ignore-next-line $this->client->indices()->create([ 'index' => $file->getBasename('.json'), 'body' => json_decode($file->getContents(), true, 512, \JSON_THROW_ON_ERROR), @@ -97,7 +95,6 @@ private function deleteIndexes(): void } if ([] !== $indexes) { - // @phpstan-ignore-next-line $this->client->indices()->delete([ 'index' => implode(',', $indexes), 'ignore_unavailable' => true, @@ -110,7 +107,6 @@ private function loadFixtures(): void $finder = new Finder(); $finder->files()->in($this->elasticsearchFixturesPath)->name('*.json'); - // @phpstan-ignore-next-line $indexClient = $this->client->indices(); foreach ($finder as $file) { @@ -127,14 +123,12 @@ private function loadFixtures(): void $bulk[] = $document; if (0 === (\count($bulk) % 50)) { - // @phpstan-ignore-next-line $this->client->bulk(['body' => $bulk]); $bulk = []; } } if ($bulk) { - // @phpstan-ignore-next-line $this->client->bulk(['body' => $bulk]); } diff --git a/tests/Behat/HydraContext.php b/tests/Behat/HydraContext.php index 1a208159690..a0425ac2b13 100644 --- a/tests/Behat/HydraContext.php +++ b/tests/Behat/HydraContext.php @@ -301,4 +301,26 @@ private function getLastJsonResponse(): \stdClass return $decoded; } + + /** + * @Then the Hydra context matches the online resource :url + */ + public function assertHydraContextIsCorrect(string $url): void + { + $opts = [ + 'http' => [ + 'method' => 'GET', + 'header' => "User-Agent: Mozilla/5.0\r\n", + ], + ]; + + $context = stream_context_create($opts); + $upstream = json_decode(file_get_contents($url, false, $context)); + $actual = $this->getLastJsonResponse(); + $local = $actual->{'@context'}[0]; + Assert::assertEquals( + $upstream, + $local + ); + } } diff --git a/tests/ConfigCacheFactory.php b/tests/ConfigCacheFactory.php new file mode 100644 index 00000000000..8d0463fa06c --- /dev/null +++ b/tests/ConfigCacheFactory.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheInterface; + +final class ConfigCacheFactory implements ConfigCacheFactoryInterface +{ + public function cache(string $file, callable $callback): ConfigCacheInterface + { + $configCache = new TestSuiteConfigCache(new ConfigCache($file, false)); + + if (!$configCache->isFresh()) { + $callback($configCache); + } + + return $configCache; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/ErrorWithOverridenStatus.php b/tests/Fixtures/TestBundle/ApiResource/ErrorWithOverridenStatus.php index 8a2d465e9a7..b7fd705024f 100644 --- a/tests/Fixtures/TestBundle/ApiResource/ErrorWithOverridenStatus.php +++ b/tests/Fixtures/TestBundle/ApiResource/ErrorWithOverridenStatus.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; use ApiPlatform\Metadata\Delete; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\Validator\ConstraintViolationList; #[Delete( diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php b/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php deleted file mode 100644 index 939259fbb37..00000000000 --- a/tests/Fixtures/TestBundle/ApiResource/Issue5896/TypeFactoryDecorator.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896; - -use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\TypeFactoryInterface; -use Symfony\Component\PropertyInfo\Type; - -class TypeFactoryDecorator implements TypeFactoryInterface -{ - public function __construct( - private readonly TypeFactoryInterface $decorated, - ) { - } - - public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array - { - if (is_a($type->getClassName(), LocalDate::class, true)) { - return [ - 'type' => 'string', - 'format' => 'date', - ]; - } - - return $this->decorated->getType($type, $format, $readableLink, $serializerContext, $schema); - } -} diff --git a/tests/Fixtures/TestBundle/ApiResource/PostWithUriVariables.php b/tests/Fixtures/TestBundle/ApiResource/PostWithUriVariables.php index b2e7ae048cd..4c3f36bb2c5 100644 --- a/tests/Fixtures/TestBundle/ApiResource/PostWithUriVariables.php +++ b/tests/Fixtures/TestBundle/ApiResource/PostWithUriVariables.php @@ -15,7 +15,7 @@ use ApiPlatform\Metadata\NotExposed; use ApiPlatform\Metadata\Post; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as ExceptionValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\Validator\ConstraintViolationList; #[NotExposed(uriTemplate: '/post_with_uri_variables/{id}')] @@ -34,6 +34,6 @@ public static function process(): self public static function provide(): void { - throw new ExceptionValidationException(new ConstraintViolationList()); + throw new ValidationException(new ConstraintViolationList()); } } diff --git a/tests/Fixtures/TestBundle/ApiResource/ValidationExceptionProblem.php b/tests/Fixtures/TestBundle/ApiResource/ValidationExceptionProblem.php index eca2615ea93..2b2757f81f2 100644 --- a/tests/Fixtures/TestBundle/ApiResource/ValidationExceptionProblem.php +++ b/tests/Fixtures/TestBundle/ApiResource/ValidationExceptionProblem.php @@ -14,14 +14,14 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource; use ApiPlatform\Metadata\Post; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Validator\ConstraintViolationList; #[Post(processor: [ValidationExceptionProblem::class, 'provide'])] #[Post(uriTemplate: '/exception_problems', processor: [ValidationExceptionProblem::class, 'provideException'])] -#[Post(uriTemplate: '/exception_problems_with_compatibility', processor: [ValidationExceptionProblem::class, 'provideException'], extraProperties: ['rfc_7807_compliant_errors' => false])] -#[Post(uriTemplate: '/exception_problems_without_prefix', normalizationContext: ['hydra_prefix' => false], processor: [ValidationExceptionProblem::class, 'provideException'], extraProperties: ['rfc_7807_compliant_errors' => true])] +#[Post(uriTemplate: '/exception_problems_with_compatibility', processor: [ValidationExceptionProblem::class, 'provideException'])] +#[Post(uriTemplate: '/exception_problems_without_prefix', normalizationContext: ['hydra_prefix' => false], processor: [ValidationExceptionProblem::class, 'provideException'])] class ValidationExceptionProblem { public static function provide(): void diff --git a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php index 9b828885a17..be6b288fba9 100644 --- a/tests/Fixtures/TestBundle/ApiResource/WithParameter.php +++ b/tests/Fixtures/TestBundle/ApiResource/WithParameter.php @@ -62,7 +62,7 @@ 'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]), 'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]), 'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]), - 'pattern' => new QueryParameter(schema: ['pattern' => '/\d/']), + 'pattern' => new QueryParameter(schema: ['pattern' => '\d']), ], provider: [self::class, 'collectionProvider'] )] diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php index 23b8bea0b1d..62a18c4ab6c 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php index acafa2a5a8d..1ca67b870ab 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlRelationDummy.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; diff --git a/tests/Fixtures/TestBundle/Document/DisableItemOperation.php b/tests/Fixtures/TestBundle/Document/DisableItemOperation.php index 973ca42ca50..2a540a8ff4c 100644 --- a/tests/Fixtures/TestBundle/Document/DisableItemOperation.php +++ b/tests/Fixtures/TestBundle/Document/DisableItemOperation.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Action\NotFoundAction; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Symfony\Action\NotFoundAction; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(operations: [new Get(controller: NotFoundAction::class, read: false, output: false), new GetCollection()])] diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index 24516bfbcd4..5c4c0dc7478 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -28,7 +28,7 @@ * @author Kévin Dunglas * @author Alexandre Delplace */ -#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]], 'standard_put' => false, 'rfc_7807_compliant_errors' => false], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] +#[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]], 'standard_put' => false], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] #[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ODM\Document] diff --git a/tests/Fixtures/TestBundle/Document/DummyValidation.php b/tests/Fixtures/TestBundle/Document/DummyValidation.php index 95f762484ea..25210cbd70f 100644 --- a/tests/Fixtures/TestBundle/Document/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Document/DummyValidation.php @@ -22,7 +22,7 @@ #[ApiResource(operations: [ new GetCollection(), new Post(uriTemplate: 'dummy_validation{._format}'), - new Post(uriTemplate: '/dummy_validation/validation_groups', validationContext: ['groups' => ['a']], extraProperties: ['rfc_7807_compliant_errors' => false]), + new Post(uriTemplate: '/dummy_validation/validation_groups', validationContext: ['groups' => ['a']]), new Post(uriTemplate: '/dummy_validation/validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator'], extraProperties: ['rfc_7807_compliant_errors' => false]), ] )] diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php index bd2794e67a5..84bc5353cc5 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php index 18d45ecfc4d..641ccd8dfa6 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathRelationDummy.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; diff --git a/tests/Fixtures/TestBundle/Document/Person.php b/tests/Fixtures/TestBundle/Document/Person.php index 21d287dc6ad..14a243c4a39 100644 --- a/tests/Fixtures/TestBundle/Document/Person.php +++ b/tests/Fixtures/TestBundle/Document/Person.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\AcademicGrade; use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -40,6 +41,11 @@ class Person #[ODM\Field(type: 'string')] public string $name; + /** @var array */ + #[ODM\Field(nullable: true)] + #[Groups(['people.pets'])] + public array $academicGrades = []; + #[Groups(['people.pets'])] #[ODM\ReferenceMany(targetDocument: PersonToPet::class, mappedBy: 'person')] public Collection|iterable $pets; diff --git a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php index 238d7a0e660..8955436b628 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php @@ -24,7 +24,7 @@ /** * Related To Dummy Friend represent an association table for a manytomany relation. */ -#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] +#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'])] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] diff --git a/tests/Fixtures/TestBundle/Document/SearchFilterParameterDocument.php b/tests/Fixtures/TestBundle/Document/SearchFilterParameter.php similarity index 72% rename from tests/Fixtures/TestBundle/Document/SearchFilterParameterDocument.php rename to tests/Fixtures/TestBundle/Document/SearchFilterParameter.php index d1b7da59538..83a66431f54 100644 --- a/tests/Fixtures/TestBundle/Document/SearchFilterParameterDocument.php +++ b/tests/Fixtures/TestBundle/Document/SearchFilterParameter.php @@ -15,13 +15,27 @@ use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\QueryParameter; use ApiPlatform\Tests\Fixtures\TestBundle\Filter\ODMSearchFilterValueTransformer; use ApiPlatform\Tests\Fixtures\TestBundle\Filter\ODMSearchTextAndDateFilter; +use ApiPlatform\Tests\Fixtures\TestBundle\Filter\QueryParameterOdmFilter; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[GetCollection( - uriTemplate: 'search_filter_parameter_document{._format}', + uriTemplate: 'search_filter_parameter{._format}', + parameters: [ + 'foo' => new QueryParameter(filter: 'app_odm_search_filter_via_parameter'), + 'fooAlias' => new QueryParameter(filter: 'app_odm_search_filter_via_parameter', property: 'foo'), + 'order[:property]' => new QueryParameter(filter: 'app_odm_search_filter_via_parameter.order_filter'), + + 'searchPartial[:property]' => new QueryParameter(filter: 'app_odm_search_filter_partial'), + 'searchExact[:property]' => new QueryParameter(filter: 'app_odm_search_filter_with_exact'), + 'searchOnTextAndDate[:property]' => new QueryParameter(filter: 'app_odm_filter_date_and_search'), + 'q' => new QueryParameter(property: 'hydra:freetextQuery'), + ] +)] +#[QueryCollection( parameters: [ 'foo' => new QueryParameter(filter: 'app_odm_search_filter_via_parameter'), 'order[:property]' => new QueryParameter(filter: 'app_odm_search_filter_via_parameter.order_filter'), @@ -35,8 +49,9 @@ #[ApiFilter(ODMSearchFilterValueTransformer::class, alias: 'app_odm_search_filter_partial', properties: ['foo' => 'partial'], arguments: ['key' => 'searchPartial'])] #[ApiFilter(ODMSearchFilterValueTransformer::class, alias: 'app_odm_search_filter_with_exact', properties: ['foo' => 'exact'], arguments: ['key' => 'searchExact'])] #[ApiFilter(ODMSearchTextAndDateFilter::class, alias: 'app_odm_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])] +#[QueryParameter(key: ':property', filter: QueryParameterOdmFilter::class)] #[ODM\Document] -class SearchFilterParameterDocument +class SearchFilterParameter { /** * @var int The id diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php index dfb1f919c21..a632d81dc83 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php index 4cdbebfb24c..8be39e7c89c 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlRelationDummy.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; diff --git a/tests/Fixtures/TestBundle/Entity/DisableItemOperation.php b/tests/Fixtures/TestBundle/Entity/DisableItemOperation.php index cbe03e664ae..bd9f9855dd2 100644 --- a/tests/Fixtures/TestBundle/Entity/DisableItemOperation.php +++ b/tests/Fixtures/TestBundle/Entity/DisableItemOperation.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Action\NotFoundAction; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Symfony\Action\NotFoundAction; use Doctrine\ORM\Mapping as ORM; #[ApiResource(operations: [new Get(controller: NotFoundAction::class, read: false, output: false), new GetCollection()])] diff --git a/tests/Fixtures/TestBundle/Entity/Dummy.php b/tests/Fixtures/TestBundle/Entity/Dummy.php index 55dce62d2ba..b7480f83693 100644 --- a/tests/Fixtures/TestBundle/Entity/Dummy.php +++ b/tests/Fixtures/TestBundle/Entity/Dummy.php @@ -27,7 +27,7 @@ * * @author Kévin Dunglas */ -#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])] +#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false])] #[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidation.php b/tests/Fixtures/TestBundle/Entity/DummyValidation.php index b62fbf385d6..6d93b1bfe42 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Entity/DummyValidation.php @@ -22,8 +22,8 @@ #[ApiResource(operations: [ new GetCollection(), new Post(uriTemplate: 'dummy_validation{._format}'), - new Post(uriTemplate: '/dummy_validation/validation_groups', validationContext: ['groups' => ['a']], extraProperties: ['rfc_7807_compliant_errors' => false]), - new Post(uriTemplate: '/dummy_validation/validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator'], extraProperties: ['rfc_7807_compliant_errors' => false]), + new Post(uriTemplate: '/dummy_validation/validation_groups', validationContext: ['groups' => ['a']]), + new Post(uriTemplate: '/dummy_validation/validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] )] #[ORM\Entity] diff --git a/tests/Fixtures/TestBundle/Entity/DummyWithCollectDenormalizationErrors.php b/tests/Fixtures/TestBundle/Entity/DummyWithCollectDenormalizationErrors.php index 07c9035a5b2..8953503d7d4 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyWithCollectDenormalizationErrors.php +++ b/tests/Fixtures/TestBundle/Entity/DummyWithCollectDenormalizationErrors.php @@ -23,8 +23,7 @@ operations: [ new Post(uriTemplate: 'dummy_collect_denormalization'), ], - collectDenormalizationErrors: true, - extraProperties: ['rfc_7807_compliant_errors' => false] + collectDenormalizationErrors: true )] #[ORM\Entity] class DummyWithCollectDenormalizationErrors diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php index 46530b96c5e..098750dc0dc 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php index 45b8139ecfe..f743fbf6432 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathRelationDummy.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\UrlGeneratorInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index 98daa727da6..c584c30596b 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -24,7 +24,7 @@ /** * Related To Dummy Friend represent an association table for a manytomany relation. */ -#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] +#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] diff --git a/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php b/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php index 12077e12551..8cd10c57c50 100644 --- a/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php +++ b/tests/Fixtures/TestBundle/Entity/SearchFilterParameter.php @@ -17,6 +17,7 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Tests\Fixtures\TestBundle\Filter\QueryParameterFilter; use ApiPlatform\Tests\Fixtures\TestBundle\Filter\SearchFilterValueTransformer; use ApiPlatform\Tests\Fixtures\TestBundle\Filter\SearchTextAndDateFilter; use Doctrine\ORM\Mapping as ORM; @@ -48,6 +49,7 @@ #[ApiFilter(SearchFilterValueTransformer::class, alias: 'app_search_filter_partial', properties: ['foo' => 'partial'], arguments: ['key' => 'searchPartial'])] #[ApiFilter(SearchFilterValueTransformer::class, alias: 'app_search_filter_with_exact', properties: ['foo' => 'exact'], arguments: ['key' => 'searchExact'])] #[ApiFilter(SearchTextAndDateFilter::class, alias: 'app_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])] +#[QueryParameter(key: ':property', filter: QueryParameterFilter::class)] #[ORM\Entity] class SearchFilterParameter { diff --git a/tests/Fixtures/TestBundle/Entity/SubresourceEmployee.php b/tests/Fixtures/TestBundle/Entity/SubresourceEmployee.php index d76304fc23d..50dfd7811b2 100644 --- a/tests/Fixtures/TestBundle/Entity/SubresourceEmployee.php +++ b/tests/Fixtures/TestBundle/Entity/SubresourceEmployee.php @@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource( - '/subresource_organizations/{subresourceOrganization}/subresource_employees', + uriTemplate: '/subresource_organizations/{subresourceOrganization}/subresource_employees', types: ['https://schema.org/Person'] )] #[ORM\Entity] diff --git a/tests/Fixtures/TestBundle/Filter/ArrayItemsFilter.php b/tests/Fixtures/TestBundle/Filter/ArrayItemsFilter.php index 4c2b9433be3..f1c6bfa770f 100644 --- a/tests/Fixtures/TestBundle/Filter/ArrayItemsFilter.php +++ b/tests/Fixtures/TestBundle/Filter/ArrayItemsFilter.php @@ -35,7 +35,7 @@ public function getDescription(string $resourceClass): array 'property' => 'csv-min-2', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'minItems' => 2, ], ], @@ -43,7 +43,7 @@ public function getDescription(string $resourceClass): array 'property' => 'csv-max-3', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'maxItems' => 3, ], ], @@ -51,7 +51,7 @@ public function getDescription(string $resourceClass): array 'property' => 'ssv-min-2', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'collectionFormat' => 'ssv', 'minItems' => 2, ], @@ -60,7 +60,7 @@ public function getDescription(string $resourceClass): array 'property' => 'tsv-min-2', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'collectionFormat' => 'tsv', 'minItems' => 2, ], @@ -69,7 +69,7 @@ public function getDescription(string $resourceClass): array 'property' => 'pipes-min-2', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'collectionFormat' => 'pipes', 'minItems' => 2, ], @@ -78,7 +78,7 @@ public function getDescription(string $resourceClass): array 'property' => 'csv-uniques', 'type' => 'array', 'required' => false, - 'swagger' => [ + 'schema' => [ 'uniqueItems' => true, ], ], diff --git a/tests/Fixtures/TestBundle/Filter/BoundsFilter.php b/tests/Fixtures/TestBundle/Filter/BoundsFilter.php index 8db3805095f..df3735878cd 100644 --- a/tests/Fixtures/TestBundle/Filter/BoundsFilter.php +++ b/tests/Fixtures/TestBundle/Filter/BoundsFilter.php @@ -35,7 +35,7 @@ public function getDescription(string $resourceClass): array 'property' => 'maximum', 'type' => 'float', 'required' => false, - 'swagger' => [ + 'schema' => [ 'maximum' => 10, ], ], @@ -43,16 +43,15 @@ public function getDescription(string $resourceClass): array 'property' => 'maximum', 'type' => 'float', 'required' => false, - 'swagger' => [ - 'maximum' => 10, - 'exclusiveMaximum' => true, + 'schema' => [ + 'exclusiveMaximum' => 10, ], ], 'minimum' => [ 'property' => 'minimum', 'type' => 'float', 'required' => false, - 'swagger' => [ + 'schema' => [ 'minimum' => 5, ], ], @@ -60,9 +59,8 @@ public function getDescription(string $resourceClass): array 'property' => 'exclusiveMinimum', 'type' => 'float', 'required' => false, - 'swagger' => [ - 'minimum' => 5, - 'exclusiveMinimum' => true, + 'schema' => [ + 'exclusiveMinimum' => 5, ], ], ]; diff --git a/tests/Fixtures/TestBundle/Filter/EnumFilter.php b/tests/Fixtures/TestBundle/Filter/EnumFilter.php index 25dbdb8413c..ef7454c931b 100644 --- a/tests/Fixtures/TestBundle/Filter/EnumFilter.php +++ b/tests/Fixtures/TestBundle/Filter/EnumFilter.php @@ -35,7 +35,7 @@ public function getDescription(string $resourceClass): array 'property' => 'enum', 'type' => 'string', 'required' => false, - 'swagger' => [ + 'schema' => [ 'enum' => ['in-enum', 'mune-ni'], ], ], diff --git a/tests/Fixtures/TestBundle/Filter/LengthFilter.php b/tests/Fixtures/TestBundle/Filter/LengthFilter.php index c45ac2f1cc0..a369bf84a96 100644 --- a/tests/Fixtures/TestBundle/Filter/LengthFilter.php +++ b/tests/Fixtures/TestBundle/Filter/LengthFilter.php @@ -35,7 +35,7 @@ public function getDescription(string $resourceClass): array 'property' => 'max-length-3', 'type' => 'string', 'required' => false, - 'swagger' => [ + 'schema' => [ 'maxLength' => 3, ], ], @@ -43,7 +43,7 @@ public function getDescription(string $resourceClass): array 'property' => 'min-length-3', 'type' => 'string', 'required' => false, - 'swagger' => [ + 'schema' => [ 'minLength' => 3, ], ], diff --git a/tests/Fixtures/TestBundle/Filter/MultipleOfFilter.php b/tests/Fixtures/TestBundle/Filter/MultipleOfFilter.php index ac850411009..de3e6d24844 100644 --- a/tests/Fixtures/TestBundle/Filter/MultipleOfFilter.php +++ b/tests/Fixtures/TestBundle/Filter/MultipleOfFilter.php @@ -35,7 +35,7 @@ public function getDescription(string $resourceClass): array 'property' => 'multiple-of', 'type' => 'float', 'required' => false, - 'swagger' => [ + 'schema' => [ 'multipleOf' => 2, ], ], diff --git a/tests/Fixtures/TestBundle/Filter/ODMSearchFilterValueTransformer.php b/tests/Fixtures/TestBundle/Filter/ODMSearchFilterValueTransformer.php index 38d66a36a8e..19d76c00f33 100644 --- a/tests/Fixtures/TestBundle/Filter/ODMSearchFilterValueTransformer.php +++ b/tests/Fixtures/TestBundle/Filter/ODMSearchFilterValueTransformer.php @@ -21,21 +21,26 @@ final class ODMSearchFilterValueTransformer implements FilterInterface { - public function __construct(#[Autowire('@api_platform.doctrine_mongodb.odm.search_filter.instance')] readonly FilterInterface $searchFilter, ?array $properties = null, private readonly ?string $key = null) + public function __construct(#[Autowire('@api_platform.doctrine_mongodb.odm.search_filter.instance')] private readonly FilterInterface $searchFilter, private ?array $properties = null, private readonly ?string $key = null) { - if ($searchFilter instanceof PropertyAwareFilterInterface) { - $searchFilter->setProperties($properties); - } } // This function is only used to hook in documentation generators (supported by Swagger and Hydra) public function getDescription(string $resourceClass): array { + if ($this->searchFilter instanceof PropertyAwareFilterInterface) { + $this->searchFilter->setProperties($this->properties); + } + return $this->searchFilter->getDescription($resourceClass); } public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { + if ($this->searchFilter instanceof PropertyAwareFilterInterface) { + $this->searchFilter->setProperties($this->properties); + } + $filterContext = ['filters' => $context['filters'][$this->key]] + $context; $this->searchFilter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); } diff --git a/tests/Fixtures/TestBundle/Filter/PatternFilter.php b/tests/Fixtures/TestBundle/Filter/PatternFilter.php index 49ac2e2e701..6a1940fe8a4 100644 --- a/tests/Fixtures/TestBundle/Filter/PatternFilter.php +++ b/tests/Fixtures/TestBundle/Filter/PatternFilter.php @@ -35,8 +35,8 @@ public function getDescription(string $resourceClass): array 'property' => 'pattern', 'type' => 'string', 'required' => false, - 'swagger' => [ - 'pattern' => '/^(pattern|nrettap)$/', + 'schema' => [ + 'pattern' => '^(pattern|nrettap)$', ], ], ]; diff --git a/tests/Fixtures/TestBundle/Filter/QueryParameterFilter.php b/tests/Fixtures/TestBundle/Filter/QueryParameterFilter.php new file mode 100644 index 00000000000..a15bada5cc6 --- /dev/null +++ b/tests/Fixtures/TestBundle/Filter/QueryParameterFilter.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; + +final class QueryParameterFilter implements FilterInterface +{ + public function getDescription(string $resourceClass): array + { + return []; + } + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + foreach ($context['filters'] as $prop => $value) { + $queryBuilder->andWhere(\sprintf('o.%s = :%1$s', $prop))->setParameter($prop, $value); + } + } +} diff --git a/tests/Fixtures/TestBundle/Filter/QueryParameterOdmFilter.php b/tests/Fixtures/TestBundle/Filter/QueryParameterOdmFilter.php new file mode 100644 index 00000000000..0182675eb73 --- /dev/null +++ b/tests/Fixtures/TestBundle/Filter/QueryParameterOdmFilter.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Filter; + +use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; + +final class QueryParameterOdmFilter implements FilterInterface +{ + public function getDescription(string $resourceClass): array + { + return []; + } + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + foreach ($context['filters'] as $prop => $value) { + $aggregationBuilder->match()->field($prop)->equals($value); + } + } +} diff --git a/tests/Fixtures/TestBundle/Filter/RequiredAllowEmptyFilter.php b/tests/Fixtures/TestBundle/Filter/RequiredAllowEmptyFilter.php index 35de5c7be46..984b257a99b 100644 --- a/tests/Fixtures/TestBundle/Filter/RequiredAllowEmptyFilter.php +++ b/tests/Fixtures/TestBundle/Filter/RequiredAllowEmptyFilter.php @@ -17,6 +17,7 @@ use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\OpenApi\Model\Parameter; use Doctrine\ORM\QueryBuilder; class RequiredAllowEmptyFilter extends AbstractFilter @@ -35,9 +36,7 @@ public function getDescription(string $resourceClass): array 'property' => 'required-allow-empty', 'type' => 'string', 'required' => true, - 'swagger' => [ - 'allowEmptyValue' => true, - ], + 'openapi' => new Parameter(in: 'query', name: 'required-allow-empty', allowEmptyValue: true), ], ]; } diff --git a/tests/Fixtures/TestBundle/Filter/SearchFilterValueTransformer.php b/tests/Fixtures/TestBundle/Filter/SearchFilterValueTransformer.php index 979824463fd..1d2484d1a87 100644 --- a/tests/Fixtures/TestBundle/Filter/SearchFilterValueTransformer.php +++ b/tests/Fixtures/TestBundle/Filter/SearchFilterValueTransformer.php @@ -22,7 +22,7 @@ final class SearchFilterValueTransformer implements FilterInterface { - public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] readonly FilterInterface $searchFilter, private ?array $properties = null, private readonly ?string $key = null) + public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] private readonly FilterInterface $searchFilter, private ?array $properties = null, private readonly ?string $key = null) { } diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeCollectionResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeCollectionResolver.php index ebfc0baf57c..f51777a7869 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeCollectionResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeCollectionResolver.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\State\Pagination\ArrayPaginator; use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery; diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemDocumentResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemDocumentResolver.php index 5f68810826d..a9e7d7e2d14 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemDocumentResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemDocumentResolver.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument; /** diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemResolver.php index 3c80313aa1d..576c1724dc5 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/DummyCustomQueryNoReadAndSerializeItemResolver.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery; /** diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistDocumentMutationResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistDocumentMutationResolver.php index d46e6a95db7..af7793bb92b 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistDocumentMutationResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistDocumentMutationResolver.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomMutation as DummyCustomMutationDocument; /** diff --git a/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistMutationResolver.php b/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistMutationResolver.php index e74e50c555b..02ddb2f69b0 100644 --- a/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistMutationResolver.php +++ b/tests/Fixtures/TestBundle/GraphQl/Resolver/SumOnlyPersistMutationResolver.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\GraphQl\Resolver; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomMutation; /** diff --git a/tests/Fixtures/TestBundle/Resources/config/api_resources/resources.yaml b/tests/Fixtures/TestBundle/Resources/config/api_resources/resources.yaml index 9eecce5816f..ba68a13d153 100644 --- a/tests/Fixtures/TestBundle/Resources/config/api_resources/resources.yaml +++ b/tests/Fixtures/TestBundle/Resources/config/api_resources/resources.yaml @@ -11,8 +11,7 @@ resources: ApiPlatform\Tests\Fixtures\TestBundle\Model\DummyAddress: operations: ApiPlatform\Metadata\GetCollection: - # TODO Remove in 4.0 - openapiContext: + openapi: x-visibility: hide ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface: diff --git a/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php b/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php index 1b9c42ff1d8..28f9ef27228 100644 --- a/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php +++ b/tests/Fixtures/TestBundle/Serializer/Denormalizer/RelatedDummyPlainIdentifierDenormalizer.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Denormalizer; -use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument; diff --git a/tests/Fixtures/TestBundle/State/EntityClassAndCustomProviderResourceProvider.php b/tests/Fixtures/TestBundle/State/EntityClassAndCustomProviderResourceProvider.php index 9aef744dd03..05a6e651998 100644 --- a/tests/Fixtures/TestBundle/State/EntityClassAndCustomProviderResourceProvider.php +++ b/tests/Fixtures/TestBundle/State/EntityClassAndCustomProviderResourceProvider.php @@ -15,8 +15,8 @@ use ApiPlatform\Doctrine\Orm\State\CollectionProvider; use ApiPlatform\Doctrine\Orm\State\ItemProvider; -use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EntityClassAndCustomProviderResource; diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index b842bbc8c47..7d55824f4de 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -11,6 +11,12 @@ declare(strict_types=1); +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use ApiPlatform\Tests\Behat\DoctrineContext; use ApiPlatform\Tests\Fixtures\TestBundle\Document\User as UserDocument; @@ -83,6 +89,12 @@ public function registerBundles(): array return $bundles; } + public function shutdown(): void + { + parent::shutdown(); + restore_exception_handler(); + } + public function getProjectDir(): string { return __DIR__; @@ -231,24 +243,15 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ } $c->prependExtensionConfig('twig', $twigConfig); - $metadataBackwardCompatibilityLayer = (bool) ($_SERVER['EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER'] ?? false); $useSymfonyListeners = (bool) ($_SERVER['USE_SYMFONY_LISTENERS'] ?? false); - $rfc7807CompliantErrors = (bool) ($_SERVER['RFC_7807_COMPLIANT_ERRORS'] ?? true); - $useQueryParameterValidator = (bool) ($_SERVER['QUERY_PARAMETER_VALIDATOR'] ?? false); - - $legacyConfig = []; - if ($metadataBackwardCompatibilityLayer) { - $legacyConfig = ['event_listeners_backward_compatibility_layer' => $metadataBackwardCompatibilityLayer]; - } - $c->prependExtensionConfig('api_platform', $legacyConfig + [ + $c->prependExtensionConfig('api_platform', [ 'mapping' => [ 'paths' => ['%kernel.project_dir%/../TestBundle/Resources/config/api_resources'], ], 'graphql' => [ 'graphql_playground' => false, ], - 'use_deprecated_json_schema_type_factory' => true, 'use_symfony_listeners' => $useSymfonyListeners, 'defaults' => [ 'pagination_client_enabled' => true, @@ -262,10 +265,13 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ 'public' => true, ], 'normalization_context' => ['skip_null_values' => false], - 'extra_properties' => [ - 'rfc_7807_compliant_errors' => $rfc7807CompliantErrors, - 'standard_put' => true, - 'use_legacy_parameter_validator' => $useQueryParameterValidator, + 'operations' => [ + Get::class, + GetCollection::class, + Post::class, + Put::class, + Patch::class, + Delete::class, ], ], 'serializer' => [ @@ -286,6 +292,11 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ $loader->load(__DIR__.'/config/config_swagger.php'); + // We reduce the amount of resources to the strict minimum to speed up tests + if (null !== ($_ENV['APP_PHPUNIT'] ?? null)) { + $loader->load(__DIR__.'/config/phpunit.yml'); + } + if ('mongodb' === $this->environment) { $c->prependExtensionConfig('api_platform', [ 'mapping' => [ diff --git a/tests/Fixtures/app/bootstrap.php b/tests/Fixtures/app/bootstrap.php index bfc20da3ac6..5a97ac9a872 100644 --- a/tests/Fixtures/app/bootstrap.php +++ b/tests/Fixtures/app/bootstrap.php @@ -13,22 +13,12 @@ date_default_timezone_set('UTC'); -// PHPUnit's autoloader -if (!file_exists($phpUnitAutoloaderPath = __DIR__.'/../../../vendor/bin/.phpunit/phpunit/vendor/autoload.php')) { - exit('PHPUnit is not installed. Please run ./vendor/bin/simple-phpunit to install it'); -} - // Increase default max nesting level allowed by XDebug for the Symfony container $xdebugMaxNestingLevel = ini_get('xdebug.max_nesting_level'); if (false !== $xdebugMaxNestingLevel && $xdebugMaxNestingLevel <= 512) { ini_set('xdebug.max_nesting_level', 512); } -$phpunitLoader = require $phpUnitAutoloaderPath; -// Don't register the PHPUnit autoloader before the normal autoloader to prevent weird issues -$phpunitLoader->unregister(); -$phpunitLoader->register(); - $loader = require __DIR__.'/../../../vendor/autoload.php'; require __DIR__.'/AppKernel.php'; diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 986b838e174..1b8fd8e874e 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -74,13 +74,12 @@ api_platform: scopes: [] exception_to_status: Symfony\Component\Serializer\Exception\ExceptionInterface: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST - ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST - ApiPlatform\Exception\FilterValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST + ApiPlatform\Metadata\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST + ApiPlatform\Metadata\Exception\FilterValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST handle_symfony_errors: true http_cache: invalidation: enabled: true - keep_legacy_inflector: false enable_link_security: true # see also defaults in AppKernel doctrine_mongodb_odm: false @@ -442,11 +441,6 @@ services: tags: - name: 'api_platform.doctrine.odm.links_handler' - ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896\TypeFactoryDecorator: - decorates: 'api_platform.json_schema.type_factory' - arguments: - $decorated: '@.inner' - ApiPlatform\Tests\Fixtures\TestBundle\Serializer\ErrorNormalizer: arguments: [ '@serializer.normalizer.problem' ] tags: diff --git a/tests/Fixtures/app/config/config_doctrine.yml b/tests/Fixtures/app/config/config_doctrine.yml index f1d5541b984..5f842d8c9f4 100644 --- a/tests/Fixtures/app/config/config_doctrine.yml +++ b/tests/Fixtures/app/config/config_doctrine.yml @@ -150,3 +150,6 @@ services: parent: 'api_platform.doctrine.orm.order_filter' arguments: [ { id: 'ASC', foo: 'DESC' } ] tags: [ 'api_platform.filter' ] + + ApiPlatform\Tests\Fixtures\TestBundle\Filter\QueryParameterFilter: + tags: [ 'api_platform.filter' ] diff --git a/tests/Fixtures/app/config/config_mongodb.yml b/tests/Fixtures/app/config/config_mongodb.yml index 975a11aab8d..f948ea309b8 100644 --- a/tests/Fixtures/app/config/config_mongodb.yml +++ b/tests/Fixtures/app/config/config_mongodb.yml @@ -174,3 +174,6 @@ services: parent: 'api_platform.doctrine_mongodb.odm.order_filter' arguments: [ { id: 'ASC', foo: 'DESC' } ] tags: [ 'api_platform.filter' ] + + ApiPlatform\Tests\Fixtures\TestBundle\Filter\QueryParameterOdmFilter: + tags: [ 'api_platform.filter' ] diff --git a/tests/Fixtures/app/config/phpunit.yml b/tests/Fixtures/app/config/phpunit.yml new file mode 100644 index 00000000000..08ea3483740 --- /dev/null +++ b/tests/Fixtures/app/config/phpunit.yml @@ -0,0 +1,11 @@ +parameters: + env(RESOURCES): '%kernel.project_dir%/var/resources.php' +services: + phpunit_resource_name_collection: + class: ApiPlatform\Tests\PhpUnitResourceNameCollectionFactory + decorates: 'api_platform.metadata.resource.name_collection_factory.cached' + arguments: + $env: '%kernel.environment%' + $classes: '%env(require:RESOURCES)%' + config_cache_factory: + class: ApiPlatform\Tests\ConfigCacheFactory diff --git a/tests/Functional/ArrayDtoTest.php b/tests/Functional/ArrayDtoTest.php index 8314782f67a..e15bea3b7b7 100644 --- a/tests/Functional/ArrayDtoTest.php +++ b/tests/Functional/ArrayDtoTest.php @@ -14,9 +14,21 @@ namespace ApiPlatform\Tests\Functional; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6211\ArrayPropertyDtoOperation; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class ArrayDtoTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ArrayPropertyDtoOperation::class]; + } + public function testWithGroupFilter(): void { $response = self::createClient()->request('GET', '/array_property_dto_operations'); diff --git a/tests/Functional/BackedEnumPropertyTest.php b/tests/Functional/BackedEnumPropertyTest.php index 90e1ab3a2ec..c30c2ee8d00 100644 --- a/tests/Functional/BackedEnumPropertyTest.php +++ b/tests/Functional/BackedEnumPropertyTest.php @@ -14,15 +14,28 @@ namespace ApiPlatform\Tests\Functional; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\Person as PersonDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PersonToPet; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Pet; use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Tools\SchemaTool; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Component\HttpClient\HttpOptions; final class BackedEnumPropertyTest extends ApiTestCase { + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [Person::class, Pet::class]; + } + public function testJson(): void { $person = $this->createPerson(); @@ -38,7 +51,7 @@ public function testJson(): void ]); } - /** @group legacy */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testGraphQl(): void { $person = $this->createPerson(); @@ -65,13 +78,12 @@ public function testGraphQl(): void ]); } - private function createPerson(): Person + private function createPerson(): Person|PersonDocument { - $this->recreateSchema(); + $this->recreateSchema([Person::class, PersonToPet::class, Pet::class]); - /** @var EntityManagerInterface $manager */ - $manager = static::getContainer()->get('doctrine')->getManager(); - $person = new Person(); + $manager = $this->getManager(); + $person = $this->isMongoDB() ? new PersonDocument() : new Person(); $person->name = 'Sonja'; $person->genderType = GenderTypeEnum::FEMALE; $manager->persist($person); @@ -79,18 +91,4 @@ private function createPerson(): Person return $person; } - - private function recreateSchema(array $options = []): void - { - self::bootKernel($options); - - /** @var EntityManagerInterface $manager */ - $manager = static::getContainer()->get('doctrine')->getManager(); - /** @var ClassMetadata[] $classes */ - $classes = $manager->getMetadataFactory()->getAllMetadata(); - $schemaTool = new SchemaTool($manager); - - @$schemaTool->dropSchema($classes); - @$schemaTool->createSchema($classes); - } } diff --git a/tests/Functional/BackedEnumResourceTest.php b/tests/Functional/BackedEnumResourceTest.php index 6bbd2b0f448..2db9a725dad 100644 --- a/tests/Functional/BackedEnumResourceTest.php +++ b/tests/Functional/BackedEnumResourceTest.php @@ -21,10 +21,21 @@ use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumStringResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\Availability; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6264\AvailabilityStatus; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Component\HttpClient\HttpOptions; final class BackedEnumResourceTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [Availability::class, AvailabilityStatus::class, BackedEnumIntegerResource::class, BackedEnumStringResource::class]; + } + public static function providerEnumItemsJson(): iterable { // Integer cases @@ -90,7 +101,7 @@ public static function providerEnumItemsJson(): iterable } } - /** @dataProvider providerEnumItemsJson */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerEnumItemsJson')] public function testItemJson(string $uri, string $mimeType, array $expected): void { self::createClient()->request('GET', $uri, ['headers' => ['Accept' => $mimeType]]); @@ -230,7 +241,7 @@ public static function providerEnums(): iterable yield 'String enum item' => [BackedEnumStringResource::class, Get::class, '_api_/backed_enum_string_resources/{id}{._format}_get']; } - /** @dataProvider providerEnums */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerEnums')] public function testOnlyGetOperationsAddedWhenNonSpecified(string $resourceClass, string $operationClass, string $operationName): void { $factory = self::getContainer()->get('api_platform.metadata.resource.metadata_collection_factory'); @@ -398,7 +409,7 @@ public static function providerCollection(): iterable ]]; } - /** @dataProvider providerCollection */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerCollection')] public function testCollection(string $mimeType, array $expected): void { self::createClient()->request('GET', '/backed_enum_integer_resources', ['headers' => ['Accept' => $mimeType]]); @@ -448,7 +459,7 @@ public static function providerItem(): iterable ]]; } - /** @dataProvider providerItem */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerItem')] public function testItem(string $mimeType, array $expected): void { self::createClient()->request('GET', '/backed_enum_integer_resources/1', ['headers' => ['Accept' => $mimeType]]); @@ -463,7 +474,7 @@ public static function provider404s(): iterable yield ['/backed_enum_integer_resources/fortytwo']; } - /** @dataProvider provider404s */ + #[\PHPUnit\Framework\Attributes\DataProvider('provider404s')] public function testItem404(string $uri): void { self::createClient()->request('GET', $uri); @@ -498,11 +509,8 @@ public static function providerEnumItemsGraphQl(): iterable } } - /** - * @dataProvider providerEnumItemsGraphQl - * - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerEnumItemsGraphQl')] + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testItemGraphql(string $query, array $variables, array $expected): void { $options = (new HttpOptions()) @@ -514,9 +522,7 @@ public function testItemGraphql(string $query, array $variables, array $expected $this->assertJsonEquals($expected); } - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testCollectionGraphQl(): void { $query = <<<'GRAPHQL' @@ -543,9 +549,7 @@ public function testCollectionGraphQl(): void ]); } - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testItemGraphQlInteger(): void { $query = <<<'GRAPHQL' diff --git a/tests/Functional/FormatTest.php b/tests/Functional/FormatTest.php index 9dbd4c15e81..5a16a525d36 100644 --- a/tests/Functional/FormatTest.php +++ b/tests/Functional/FormatTest.php @@ -14,9 +14,21 @@ namespace ApiPlatform\Tests\Functional; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6384\AcceptHtml; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class FormatTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [AcceptHtml::class]; + } + public function testShouldReturnHtml(): void { $r = self::createClient()->request('GET', '/accept_html', ['headers' => ['Accept' => 'text/html']]); diff --git a/tests/Functional/Parameters/DoctrineTest.php b/tests/Functional/Parameters/DoctrineTest.php index ec9b75040b4..c0ba47b3fd1 100644 --- a/tests/Functional/Parameters/DoctrineTest.php +++ b/tests/Functional/Parameters/DoctrineTest.php @@ -14,24 +14,32 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; -use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameterDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SearchFilterParameter; -use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Tools\SchemaTool; -use Symfony\Component\DependencyInjection\ContainerInterface; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class DoctrineTest extends ApiTestCase { + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [SearchFilterParameter::class, FilterWithStateOptions::class]; + } + public function testDoctrineEntitySearchFilter(): void { - static::bootKernel(); - $container = static::$kernel->getContainer(); - $resource = $this->isMongoDb($container) ? SearchFilterParameterDocument::class : SearchFilterParameter::class; - $this->recreateSchema($resource); - $this->createFixture($resource); - $route = $this->isMongoDb($container) ? 'search_filter_parameter_document' : 'search_filter_parameter'; + $resource = $this->isMongoDB() ? SearchFilterParameterDocument::class : SearchFilterParameter::class; + $this->recreateSchema([$resource]); + $this->loadFixtures($resource); + $route = 'search_filter_parameter'; $response = self::createClient()->request('GET', $route.'?foo=bar'); $a = $response->toArray(); $this->assertCount(2, $a['hydra:member']); @@ -39,12 +47,11 @@ public function testDoctrineEntitySearchFilter(): void $this->assertEquals('bar', $a['hydra:member'][1]['foo']); $this->assertArraySubset(['hydra:search' => [ - 'hydra:template' => \sprintf('/%s{?foo,fooAlias,order[order[id]],order[order[foo]],searchPartial[foo],searchExact[foo],searchOnTextAndDate[foo],searchOnTextAndDate[createdAt][before],searchOnTextAndDate[createdAt][strictly_before],searchOnTextAndDate[createdAt][after],searchOnTextAndDate[createdAt][strictly_after],q}', $route), - 'hydra:mapping' => [ - ['@type' => 'IriTemplateMapping', 'variable' => 'foo', 'property' => 'foo'], - ], + 'hydra:template' => \sprintf('/%s{?foo,fooAlias,order[order[id]],order[order[foo]],searchPartial[foo],searchExact[foo],searchOnTextAndDate[foo],searchOnTextAndDate[createdAt][before],searchOnTextAndDate[createdAt][strictly_before],searchOnTextAndDate[createdAt][after],searchOnTextAndDate[createdAt][strictly_after],q,id,createdAt}', $route), ]], $a); + $this->assertArraySubset(['@type' => 'IriTemplateMapping', 'variable' => 'fooAlias', 'property' => 'foo'], $a['hydra:search']['hydra:mapping'][1]); + $response = self::createClient()->request('GET', $route.'?fooAlias=baz'); $a = $response->toArray(); $this->assertCount(1, $a['hydra:member']); @@ -66,21 +73,16 @@ public function testDoctrineEntitySearchFilter(): void $this->assertArraySubset(['foo' => 'bar', 'createdAt' => '2024-01-21T00:00:00+00:00'], $members[0]); } - /** - * @group legacy - */ public function testGraphQl(): void { if ($_SERVER['EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER'] ?? false) { $this->markTestSkipped('Parameters are not supported in BC mode.'); } - static::bootKernel(); - $container = static::$kernel->getContainer(); - $resource = $this->isMongoDb($container) ? SearchFilterParameterDocument::class : SearchFilterParameter::class; - $this->recreateSchema($resource); - $this->createFixture($resource); - $object = $this->isMongoDb($container) ? 'searchFilterParameterDocuments' : 'searchFilterParameters'; + $resource = $this->isMongoDB() ? SearchFilterParameterDocument::class : SearchFilterParameter::class; + $this->recreateSchema([$resource]); + $this->loadFixtures($resource); + $object = 'searchFilterParameters'; $response = self::createClient()->request('POST', '/graphql', ['json' => [ 'query' => \sprintf('{ %s(foo: "bar") { edges { node { id foo createdAt } } } }', $object), ]]); @@ -102,12 +104,28 @@ public function testGraphQl(): void $this->assertArraySubset(['foo' => 'bar', 'createdAt' => '2024-01-21T00:00:00+00:00'], $response->toArray()['data'][$object]['edges'][0]['node']); } + public function testPropertyPlaceholderFilter(): void + { + static::bootKernel(); + $resource = $this->isMongoDB() ? SearchFilterParameterDocument::class : SearchFilterParameter::class; + $this->recreateSchema([$resource]); + $this->loadFixtures($resource); + $route = 'search_filter_parameter'; + $response = self::createClient()->request('GET', $route.'?foo=baz'); + $a = $response->toArray(); + $this->assertEquals($a['hydra:member'][0]['foo'], 'baz'); + } + public function testStateOptions(): void { + if ($this->isMongoDB()) { + $this->markTestSkipped('Not tested with mongodb.'); + } + static::bootKernel(); $container = static::$kernel->getContainer(); - $this->recreateSchema(FilterWithStateOptionsEntity::class); - $registry = $this->isMongoDb($container) ? $container->get('doctrine_mongodb') : $container->get('doctrine'); + $this->recreateSchema([FilterWithStateOptionsEntity::class]); + $registry = $container->get('doctrine'); $manager = $registry->getManager(); $d = new \DateTimeImmutable(); $manager->persist(new FilterWithStateOptionsEntity(dummyDate: $d, name: 'current')); @@ -125,27 +143,10 @@ public function testStateOptions(): void $this->assertEquals('after', $a['hydra:member'][0]['name']); } - private function recreateSchema(string $resourceClass): void + public function loadFixtures(string $resourceClass): void { $container = static::$kernel->getContainer(); - $registry = $this->isMongoDb($container) ? $container->get('doctrine_mongodb') : $container->get('doctrine'); - $manager = $registry->getManager(); - - if ($manager instanceof EntityManagerInterface) { - $classes = $manager->getClassMetadata($resourceClass); - $schemaTool = new SchemaTool($manager); - @$schemaTool->dropSchema([$classes]); - @$schemaTool->createSchema([$classes]); - } elseif ($manager instanceof DocumentManager) { - $schemaManager = $manager->getSchemaManager(); - $schemaManager->dropCollections(); - } - } - - public function createFixture(string $resourceClass): void - { - $container = static::$kernel->getContainer(); - $registry = $this->isMongoDb($container) ? $container->get('doctrine_mongodb') : $container->get('doctrine'); + $registry = $this->isMongoDB() ? $container->get('doctrine_mongodb') : $container->get('doctrine'); $manager = $registry->getManager(); $date = new \DateTimeImmutable('2024-01-21'); foreach (['foo', 'foo', 'foo', 'bar', 'bar', 'baz'] as $t) { @@ -158,11 +159,7 @@ public function createFixture(string $resourceClass): void $manager->persist($s); } - $manager->flush(); - } - private function isMongoDb(ContainerInterface $container): bool - { - return 'mongodb' === $container->getParameter('kernel.environment'); + $manager->flush(); } } diff --git a/tests/Functional/Parameters/HydraTest.php b/tests/Functional/Parameters/HydraTest.php index c07da975db6..579d94ee6d9 100644 --- a/tests/Functional/Parameters/HydraTest.php +++ b/tests/Functional/Parameters/HydraTest.php @@ -14,9 +14,21 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class HydraTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [WithParameter::class]; + } + public function testHydraTemplate(): void { $response = self::createClient()->request('GET', 'with_parameters_collection?hydra=1'); diff --git a/tests/Functional/Parameters/ParameterProviderTest.php b/tests/Functional/Parameters/ParameterProviderTest.php index b04bfa72527..0f5f6840bd8 100644 --- a/tests/Functional/Parameters/ParameterProviderTest.php +++ b/tests/Functional/Parameters/ParameterProviderTest.php @@ -14,9 +14,21 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6673\MutlipleParameterProvider; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class ParameterProviderTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [MutlipleParameterProvider::class]; + } + public function testMultipleParameterProviderShouldChangeTheOperation(): void { $response = self::createClient()->request('GET', 'issue6673_multiple_parameter_provider?a=1&b=2', ['headers' => ['accept' => 'application/json']]); diff --git a/tests/Functional/Parameters/ParameterTest.php b/tests/Functional/Parameters/ParameterTest.php index fa029365b2d..8f99c052ca9 100644 --- a/tests/Functional/Parameters/ParameterTest.php +++ b/tests/Functional/Parameters/ParameterTest.php @@ -14,9 +14,21 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class ParameterTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [WithParameter::class]; + } + public function testWithGroupFilter(): void { $response = self::createClient()->request('GET', 'with_parameters/1?groups[]=b'); @@ -57,9 +69,9 @@ public function testDisabled(): void /** * Because of the openapiContext deprecation. - * - * @group legacy + * TODO: only select a few classes to generate the docs for a faster test. */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testDisableOpenApi(): void { $response = self::createClient()->request('GET', 'docs', ['headers' => ['accept' => 'application/vnd.openapi+json']]); diff --git a/tests/Functional/Parameters/QueryParameterStateOptionsTest.php b/tests/Functional/Parameters/QueryParameterStateOptionsTest.php index ba60820b6f8..a415f7632a7 100644 --- a/tests/Functional/Parameters/QueryParameterStateOptionsTest.php +++ b/tests/Functional/Parameters/QueryParameterStateOptionsTest.php @@ -14,14 +14,26 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AgentApi; use ApiPlatform\Tests\Fixtures\TestBundle\Document\AgentDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Agent; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\SchemaTool; final class QueryParameterStateOptionsTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [AgentApi::class]; + } + public function testQueryParameterStateOptions(): void { $this->recreateSchema(); diff --git a/tests/Functional/Parameters/SecurityTest.php b/tests/Functional/Parameters/SecurityTest.php index 6c25a8f4e22..0bb29dea66d 100644 --- a/tests/Functional/Parameters/SecurityTest.php +++ b/tests/Functional/Parameters/SecurityTest.php @@ -14,18 +14,30 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithSecurityParameter; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\User\InMemoryUser; -class SecurityTest extends ApiTestCase +final class SecurityTest extends ApiTestCase { - public function dataUserAuthorization(): iterable + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [WithSecurityParameter::class]; + } + + public static function dataUserAuthorization(): iterable { yield [['ROLE_ADMIN'], Response::HTTP_OK]; yield [['ROLE_USER'], Response::HTTP_FORBIDDEN]; } - /** @dataProvider dataUserAuthorization */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataUserAuthorization')] public function testUserAuthorization(array $roles, int $expectedStatusCode): void { $client = self::createClient(); @@ -44,13 +56,13 @@ public function testNoValueParameter(): void $this->assertResponseIsSuccessful(); } - public function dataSecurityValues(): iterable + public static function dataSecurityValues(): iterable { yield ['secured', Response::HTTP_OK]; yield ['not_the_expected_parameter_value', Response::HTTP_UNAUTHORIZED]; } - /** @dataProvider dataSecurityValues */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataSecurityValues')] public function testSecurityHeaderValues(string $parameterValue, int $expectedStatusCode): void { self::createClient()->request('GET', 'with_security_parameters_collection', [ @@ -61,7 +73,7 @@ public function testSecurityHeaderValues(string $parameterValue, int $expectedSt $this->assertResponseStatusCodeSame($expectedStatusCode); } - /** @dataProvider dataSecurityValues */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataSecurityValues')] public function testSecurityQueryValues(string $parameterValue, int $expectedStatusCode): void { self::createClient()->request('GET', \sprintf('with_security_parameters_collection?secret=%s', $parameterValue)); diff --git a/tests/Functional/Parameters/ValidationTest.php b/tests/Functional/Parameters/ValidationTest.php index 26edc53846e..7ae22b9a00d 100644 --- a/tests/Functional/Parameters/ValidationTest.php +++ b/tests/Functional/Parameters/ValidationTest.php @@ -14,9 +14,22 @@ namespace ApiPlatform\Tests\Functional\Parameters; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ValidateParameterBeforeProvider; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\WithParameter; +use ApiPlatform\Tests\SetupClassResourcesTrait; final class ValidationTest extends ApiTestCase { + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [WithParameter::class, ValidateParameterBeforeProvider::class]; + } + public function testWithGroupFilter(): void { $response = self::createClient()->request('GET', 'with_parameters_collection'); @@ -26,10 +39,9 @@ public function testWithGroupFilter(): void } /** - * @dataProvider provideQueryStrings - * * @param array $expectedViolations */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideQueryStrings')] public function testValidation(string $queryString, array $expectedViolations): void { $response = self::createClient()->request('GET', 'validate_parameters?'.$queryString); @@ -38,7 +50,7 @@ public function testValidation(string $queryString, array $expectedViolations): ], $response->toArray(false)); } - public function provideQueryStrings(): array + public static function provideQueryStrings(): array { return [ [ @@ -101,7 +113,7 @@ public function provideQueryStrings(): array public function testBlank(): void { - $response = self::createClient()->request('GET', 'validate_parameters?blank=f'); + self::createClient()->request('GET', 'validate_parameters?blank=f'); $this->assertResponseIsSuccessful(); } @@ -134,4 +146,10 @@ public function testValidatePropertyPlaceholder(): void ], ], $response->toArray(false)); } + + public function testValidatePattern(): void + { + self::createClient()->request('GET', 'validate_parameters?pattern=2'); + $this->assertResponseIsSuccessful(); + } } diff --git a/tests/Hal/JsonSchema/SchemaFactoryTest.php b/tests/Hal/JsonSchema/SchemaFactoryTest.php index b59ffef7f8b..8868ab3d0cf 100644 --- a/tests/Hal/JsonSchema/SchemaFactoryTest.php +++ b/tests/Hal/JsonSchema/SchemaFactoryTest.php @@ -53,7 +53,6 @@ protected function setUp(): void $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); $baseSchemaFactory = new BaseSchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactory->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), propertyMetadataFactory: $propertyMetadataFactory->reveal(), diff --git a/tests/Hal/Serializer/CollectionNormalizerTest.php b/tests/Hal/Serializer/CollectionNormalizerTest.php index d30cf4d4d6b..c08caaffb6d 100644 --- a/tests/Hal/Serializer/CollectionNormalizerTest.php +++ b/tests/Hal/Serializer/CollectionNormalizerTest.php @@ -13,19 +13,18 @@ namespace ApiPlatform\Tests\Hal\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Hal\Serializer\CollectionNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * @author Kévin Dunglas @@ -34,9 +33,7 @@ class CollectionNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testSupportsNormalize(): void { $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); @@ -55,10 +52,6 @@ public function testSupportsNormalize(): void 'native-array' => true, '\Traversable' => true, ], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalizePaginator(): void diff --git a/tests/Hal/Serializer/EntrypointNormalizerTest.php b/tests/Hal/Serializer/EntrypointNormalizerTest.php index a1af82bda2e..e4ac3eaeadf 100644 --- a/tests/Hal/Serializer/EntrypointNormalizerTest.php +++ b/tests/Hal/Serializer/EntrypointNormalizerTest.php @@ -13,8 +13,7 @@ namespace ApiPlatform\Tests\Hal\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Hal\Serializer\EntrypointNormalizer; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; @@ -27,7 +26,6 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Serializer; /** * @author Kévin Dunglas @@ -36,9 +34,6 @@ class EntrypointNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportNormalization(): void { $collection = new ResourceNameCollection(); @@ -55,11 +50,7 @@ public function testSupportNormalization(): void $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), EntrypointNormalizer::FORMAT)); $this->assertEmpty($normalizer->getSupportedTypes('json')); - $this->assertSame([Entrypoint::class => true, DocumentationEntrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } + $this->assertSame([Entrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); } public function testNormalize(): void diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index 071481201fd..b45e6b40337 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -73,9 +73,7 @@ public function testDoesNotSupportDenormalization(): void $normalizer->denormalize(['foo'], 'Foo'); } - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testSupportsNormalization(): void { $std = new \stdClass(); @@ -106,10 +104,6 @@ public function testSupportsNormalization(): void $this->assertFalse($normalizer->supportsNormalization($std, $normalizer::FORMAT)); $this->assertEmpty($normalizer->getSupportedTypes('xml')); $this->assertSame(['object' => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalize(): void diff --git a/tests/Hal/Serializer/ObjectNormalizerTest.php b/tests/Hal/Serializer/ObjectNormalizerTest.php index 5215714635f..ed89b9fe812 100644 --- a/tests/Hal/Serializer/ObjectNormalizerTest.php +++ b/tests/Hal/Serializer/ObjectNormalizerTest.php @@ -45,9 +45,7 @@ public function testDoesNotSupportDenormalization(): void $normalizer->denormalize(['foo'], 'Foo'); } - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testSupportsNormalization(): void { $std = new \stdClass(); diff --git a/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php b/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php deleted file mode 100644 index 674e9f24879..00000000000 --- a/tests/Hydra/EventListener/AddLinkHeaderListenerTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Hydra\EventListener; - -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Hydra\EventListener\AddLinkHeaderListener; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\WebLink\HttpHeaderSerializer; - -/** - * @author Kévin Dunglas - */ -class AddLinkHeaderListenerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider provider - */ - public function testAddLinkHeader(string $expected, Request $request): void - { - $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); - $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL)->willReturn('http://example.com/docs')->shouldBeCalled(); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new Response() - ); - - $listener = new AddLinkHeaderListener($urlGenerator->reveal()); - $listener->onKernelResponse($event); - $this->assertSame($expected, (new HttpHeaderSerializer())->serialize($request->attributes->get('_api_platform_links')->getLinks())); - } - - public static function provider(): \Iterator - { - yield ['; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', new Request()]; - } - - public function testSkipWhenPreflightRequest(): void - { - $request = new Request(); - $request->setMethod('OPTIONS'); - $request->headers->set('Access-Control-Request-Method', 'POST'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new Response() - ); - - $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); - $listener = new AddLinkHeaderListener($urlGenerator->reveal()); - $listener->onKernelResponse($event); - - $this->assertFalse($request->attributes->has('_api_platform_links')); - } -} diff --git a/tests/JsonApi/Serializer/ErrorNormalizerTest.php b/tests/JsonApi/Serializer/ErrorNormalizerTest.php deleted file mode 100644 index 71e7a393b11..00000000000 --- a/tests/JsonApi/Serializer/ErrorNormalizerTest.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\JsonApi\Serializer; - -use ApiPlatform\JsonApi\Serializer\ErrorNormalizer; -use ApiPlatform\Tests\Mock\Exception\ErrorCodeSerializable; -use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Serializer\Serializer; - -/** - * @author Baptiste Meyer - */ -class ErrorNormalizerTest extends TestCase -{ - /** - * @group legacy - */ - public function testSupportsNormalization(): void - { - $normalizer = new ErrorNormalizer(); - - $this->assertTrue($normalizer->supportsNormalization(new \Exception(), ErrorNormalizer::FORMAT)); - $this->assertFalse($normalizer->supportsNormalization(new \Exception(), 'xml')); - $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); - - $this->assertTrue($normalizer->supportsNormalization(new FlattenException(), ErrorNormalizer::FORMAT)); - $this->assertFalse($normalizer->supportsNormalization(new FlattenException(), 'xml')); - $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); - $this->assertEmpty($normalizer->getSupportedTypes('json')); - $this->assertSame([ - \Exception::class => true, - FlattenException::class => true, - ], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } - } - - /** - * @dataProvider errorProvider - * - * @group legacy - * - * @param int $status http status code of the Exception - * @param string $originalMessage original message of the Exception - * @param bool $debug simulates kernel debug variable - */ - public function testNormalize(int $status, string $originalMessage, bool $debug): void - { - $normalizer = new ErrorNormalizer($debug); - $exception = FlattenException::create(new \Exception($originalMessage), $status); - - $expected = [ - 'title' => 'An error occurred', - 'description' => ($debug || $status < 500) ? $originalMessage : Response::$statusTexts[$status], - ]; - - if ($debug) { - $expected['trace'] = $exception->getTrace(); - } - - $this->assertSame($expected, $normalizer->normalize($exception, ErrorNormalizer::FORMAT, ['statusCode' => $status])); - } - - /** - * @group legacy - */ - public function testNormalizeAnExceptionWithCustomErrorCode(): void - { - $status = Response::HTTP_BAD_REQUEST; - $originalMessage = 'my-message'; - $debug = false; - - $normalizer = new ErrorNormalizer($debug); - $exception = new ErrorCodeSerializable($originalMessage); - - $expected = [ - 'title' => 'An error occurred', - 'description' => 'my-message', - 'code' => ErrorCodeSerializable::getErrorCode(), - ]; - - $this->assertSame($expected, $normalizer->normalize($exception, ErrorNormalizer::FORMAT, ['statusCode' => $status])); - } - - /** - * @group legacy - */ - public function testNormalizeAFlattenExceptionWithCustomErrorCode(): void - { - $status = Response::HTTP_BAD_REQUEST; - $originalMessage = 'my-message'; - $debug = false; - - $normalizer = new ErrorNormalizer($debug); - $exception = FlattenException::create(new ErrorCodeSerializable($originalMessage), $status); - - $expected = [ - 'title' => 'An error occurred', - 'description' => 'my-message', - 'code' => ErrorCodeSerializable::getErrorCode(), - ]; - - $this->assertSame($expected, $normalizer->normalize($exception, ErrorNormalizer::FORMAT, ['statusCode' => $status])); - } - - public static function errorProvider(): array - { - return [ - [Response::HTTP_INTERNAL_SERVER_ERROR, 'Sensitive SQL error displayed', false], - [Response::HTTP_GATEWAY_TIMEOUT, 'Sensitive server error displayed', false], - [Response::HTTP_BAD_REQUEST, 'Bad Request Message', false], - [Response::HTTP_INTERNAL_SERVER_ERROR, 'Sensitive SQL error displayed', true], - [Response::HTTP_GATEWAY_TIMEOUT, 'Sensitive server error displayed', true], - [Response::HTTP_BAD_REQUEST, 'Bad Request Message', true], - ]; - } -} diff --git a/tests/JsonLd/ContextBuilderTest.php b/tests/JsonLd/ContextBuilderTest.php index 34fe89535f6..e5ba3a59326 100644 --- a/tests/JsonLd/ContextBuilderTest.php +++ b/tests/JsonLd/ContextBuilderTest.php @@ -35,6 +35,8 @@ use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\PropertyInfo\Type; +use const ApiPlatform\JsonLd\HYDRA_CONTEXT; + /** * @author Markus Mächler */ @@ -291,7 +293,7 @@ public function testResourceContextWithoutHydraPrefix(): void $contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal(), null, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false]); $expected = [ - 'http://www.w3.org/ns/hydra/context.jsonld', + HYDRA_CONTEXT, [ '@vocab' => '#', 'hydra' => 'http://www.w3.org/ns/hydra/core#', diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index 3f64481dc6a..a3ec829b632 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -13,8 +13,26 @@ namespace ApiPlatform\Tests\JsonSchema\Command; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Animal; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AnimalObservation; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumIntegerResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\BackedEnumStringResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\BrokenDocs; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6299\Issue6299; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6317\Issue6317; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithEnumProperty; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Species; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Answer; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\BagOfTests; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998\Issue5998Product; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998\ProductCode; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998\SaveProduct; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\ApplicationTester; @@ -24,8 +42,8 @@ */ class JsonSchemaGenerateCommandTest extends KernelTestCase { + use SetupClassResourcesTrait; private ApplicationTester $tester; - private string $entityClass; protected function setUp(): void @@ -40,6 +58,32 @@ protected function setUp(): void $this->tester = new ApplicationTester($application); } + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ + Dummy::class, + BrokenDocs::class, + Nest::class, + BagOfTests::class, + ResourceWithEnumProperty::class, + Issue6299::class, + RelatedDummy::class, + Question::class, + Answer::class, + AnimalObservation::class, + Animal::class, + Species::class, + Issue6317::class, + ProductCode::class, + Issue5998Product::class, + BackedEnumIntegerResource::class, + BackedEnumStringResource::class, + ]; + } + public function testExecuteWithoutOption(): void { $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass]); @@ -88,7 +132,7 @@ public function testExecuteWithJsonldTypeInput(): void */ public function testExecuteWithNotExposedResourceAndReadableLink(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5501\BrokenDocs', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => BrokenDocs::class, '--type' => 'output']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('Related.jsonld-location.read_collection', $result); @@ -97,9 +141,10 @@ public function testExecuteWithNotExposedResourceAndReadableLink(): void /** * When serializer groups are present the Schema should have an embed resource. #5470 breaks array references when serializer groups are present. */ + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testArraySchemaWithReference(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5793\BagOfTests', '--type' => 'input']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => BagOfTests::class, '--type' => 'input']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -126,7 +171,7 @@ public function testArraySchemaWithReference(): void public function testArraySchemaWithMultipleUnionTypesJsonLd(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest', '--type' => 'output', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Nest::class, '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -142,7 +187,7 @@ public function testArraySchemaWithMultipleUnionTypesJsonLd(): void public function testArraySchemaWithMultipleUnionTypesJsonApi(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest', '--type' => 'output', '--format' => 'jsonapi']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Nest::class, '--type' => 'output', '--format' => 'jsonapi']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -158,7 +203,7 @@ public function testArraySchemaWithMultipleUnionTypesJsonApi(): void public function testArraySchemaWithMultipleUnionTypesJsonHal(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6212\Nest', '--type' => 'output', '--format' => 'jsonhal']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Nest::class, '--type' => 'output', '--format' => 'jsonhal']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -172,28 +217,12 @@ public function testArraySchemaWithMultipleUnionTypesJsonHal(): void $this->assertArrayHasKey('Robin.jsonhal', $json['definitions']); } - /** - * TODO: add deprecation (TypeFactory will be deprecated in api platform 3.3). - * - * @group legacy - */ - public function testArraySchemaWithTypeFactory(): void - { - $container = static::getContainer(); - - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5896\Foo', '--type' => 'output']); - $result = $this->tester->getDisplay(); - $json = json_decode($result, associative: true); - - $this->assertEquals($json['definitions']['Foo.jsonld']['properties']['expiration'], ['type' => 'string', 'format' => 'date']); - } - /** * Test issue #5998. */ public function testWritableNonResourceRef(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5998\SaveProduct', '--type' => 'input']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => SaveProduct::class, '--type' => 'input']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -205,7 +234,7 @@ public function testWritableNonResourceRef(): void */ public function testOpenApiResourceRefIsNotOverwritten(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6299\Issue6299', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Issue6299::class, '--type' => 'output']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); @@ -216,18 +245,20 @@ public function testOpenApiResourceRefIsNotOverwritten(): void /** * Test related Schema keeps json-ld context. */ + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testSubSchemaJsonLd(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy', '--type' => 'output', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => RelatedDummy::class, '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $this->assertArrayHasKey('@id', $json['definitions']['ThirdLevel.jsonld-friends']['properties']); } + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testJsonApiIncludesSchema(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question', '--type' => 'output', '--format' => 'jsonapi']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Question::class, '--type' => 'output', '--format' => 'jsonapi']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['Question.jsonapi']['properties']['data']['properties']; @@ -239,7 +270,7 @@ public function testJsonApiIncludesSchema(): void $this->assertArrayHasKey('$ref', $included['items']['anyOf'][0]); $this->assertSame('#/definitions/Answer.jsonapi', $included['items']['anyOf'][0]['$ref']); - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AnimalObservation', '--type' => 'output', '--format' => 'jsonapi']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => AnimalObservation::class, '--type' => 'output', '--format' => 'jsonapi']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['AnimalObservation.jsonapi']['properties']['data']['properties']; @@ -251,7 +282,7 @@ public function testJsonApiIncludesSchema(): void $this->assertCount(1, $included['items']['anyOf']); $this->assertSame('#/definitions/Animal.jsonapi', $included['items']['anyOf'][0]['$ref']); - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Animal', '--type' => 'output', '--format' => 'jsonapi']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Animal::class, '--type' => 'output', '--format' => 'jsonapi']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['Animal.jsonapi']['properties']['data']['properties']; @@ -263,7 +294,7 @@ public function testJsonApiIncludesSchema(): void $this->assertCount(1, $included['items']['anyOf']); $this->assertSame('#/definitions/Species.jsonapi', $included['items']['anyOf'][0]['$ref']); - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Species', '--type' => 'output', '--format' => 'jsonapi']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Species::class, '--type' => 'output', '--format' => 'jsonapi']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['Species.jsonapi']['properties']['data']['properties']; @@ -277,7 +308,7 @@ public function testJsonApiIncludesSchema(): void */ public function testBackedEnumExamplesAreNotLost(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6317\Issue6317', '--type' => 'output', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => Issue6317::class, '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['Issue6317.jsonld']['properties']; @@ -292,7 +323,7 @@ public function testBackedEnumExamplesAreNotLost(): void public function testResourceWithEnumPropertiesSchema(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => 'ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithEnumProperty', '--type' => 'output', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => ResourceWithEnumProperty::class, '--type' => 'output', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $json = json_decode($result, associative: true); $properties = $json['definitions']['ResourceWithEnumProperty.jsonld']['properties']; diff --git a/tests/JsonSchema/DefinitionNameFactoryTest.php b/tests/JsonSchema/DefinitionNameFactoryTest.php index 519fcfcad42..b02159e12a4 100644 --- a/tests/JsonSchema/DefinitionNameFactoryTest.php +++ b/tests/JsonSchema/DefinitionNameFactoryTest.php @@ -62,7 +62,7 @@ public static function providerDefinitions(): iterable yield ['Bar.DtoOutput.jsonld-read_write', Dummy::class, 'jsonld', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; } - /** @dataProvider providerDefinitions */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerDefinitions')] public function testCreate(string $expected, string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): void { $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); diff --git a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php b/tests/OpenApi/Command/OpenApiCommandTest.php similarity index 85% rename from tests/Symfony/Bundle/Command/OpenApiCommandTest.php rename to tests/OpenApi/Command/OpenApiCommandTest.php index cf68f461997..24baa196521 100644 --- a/tests/Symfony/Bundle/Command/OpenApiCommandTest.php +++ b/tests/OpenApi/Command/OpenApiCommandTest.php @@ -11,10 +11,13 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Symfony\Bundle\Command; +namespace ApiPlatform\Tests\OpenApi\Command; +use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\DummyCar; use ApiPlatform\OpenApi\OpenApi; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6317\Issue6317; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5625\Currency; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Tester\ApplicationTester; @@ -23,14 +26,10 @@ /** * @author Amrouche Hamza - * - * TODO Remove group legacy in 4.0 - * - * @group legacy */ class OpenApiCommandTest extends KernelTestCase { - use ExpectDeprecationTrait; + use SetupClassResourcesTrait; private ApplicationTester $tester; @@ -42,8 +41,18 @@ protected function setUp(): void $application->setCatchExceptions(false); $application->setAutoExit(false); $this->tester = new ApplicationTester($application); + } - $this->handleDeprecations(); + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ + DummyCar::class, + Issue6317::class, + Currency::class, + ]; } public function testExecute(): void @@ -53,8 +62,10 @@ public function testExecute(): void $this->assertJson($this->tester->getDisplay()); } + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testExecuteWithYaml(): void { + // $this->setMetadataClasses([DummyCar::class, Currency::class]); $this->tester->run(['command' => 'api:openapi:export', '--yaml' => true]); $result = $this->tester->getDisplay(); @@ -108,6 +119,7 @@ public function testExecuteWithYaml(): void public function testWriteToFile(): void { + // $this->setMetadataClasses([DummyCar::class]); /** @var string $tmpFile */ $tmpFile = tempnam(sys_get_temp_dir(), 'test_write_to_file'); @@ -122,6 +134,7 @@ public function testWriteToFile(): void */ public function testBackedEnumExamplesAreNotLost(): void { + // $this->setMetadataClasses([Issue6317::class]); $this->tester->run(['command' => 'api:openapi:export']); $result = $this->tester->getDisplay(); $json = json_decode($result, true, 512, \JSON_THROW_ON_ERROR); @@ -148,12 +161,4 @@ private function assertYaml(string $data): void } $this->addToAssertionCount(1); } - - /** - * TODO Remove in 4.0. - */ - private function handleDeprecations(): void - { - $this->expectDeprecation('Since api-platform/core 3.1: The "%s" option is deprecated, use "openapi" instead.'); - } } diff --git a/tests/PhpUnitResourceNameCollectionFactory.php b/tests/PhpUnitResourceNameCollectionFactory.php new file mode 100644 index 00000000000..8f65b82d7b6 --- /dev/null +++ b/tests/PhpUnitResourceNameCollectionFactory.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\State\ApiResource\Error; +use ApiPlatform\Validator\Exception\ValidationException; + +/** + * Replaces the AttributeResourceNameCollectionFactory to speed up tests. + */ +final class PhpUnitResourceNameCollectionFactory implements ResourceNameCollectionFactoryInterface +{ + /** + * @param class-string[] $classes + */ + public function __construct(private readonly string $env, private readonly array $classes) + { + } + + public function create(): ResourceNameCollection + { + /* @var array */ + $classes = []; + foreach ($this->classes as $c) { + if ('mongodb' === $this->env) { + $c = str_contains($c, 'Entity') ? str_replace('Entity', 'Document', $c) : $c; + } + + if (!class_exists($c) && !interface_exists($c)) { + continue; + } + + $classes[$c] = true; + } + + $classes[Error::class] = true; + $classes[ValidationException::class] = true; + + return new ResourceNameCollection(array_keys($classes)); + } +} diff --git a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php index 9f0d4cbe823..f6e4039c5a2 100644 --- a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php @@ -19,7 +19,6 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; @@ -32,9 +31,7 @@ class ConstraintViolationNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testSupportNormalization(): void { $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); @@ -46,15 +43,9 @@ public function testSupportNormalization(): void $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); $this->assertEmpty($normalizer->getSupportedTypes('json')); $this->assertSame([ConstraintViolationListInterface::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } - /** - * @dataProvider nameConverterProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('nameConverterProvider')] public function testNormalize(callable $nameConverterFactory, array $expected): void { $normalizer = new ConstraintViolationListNormalizer(['severity', 'anotherField1'], $nameConverterFactory($this)); @@ -73,24 +64,19 @@ public function testNormalize(callable $nameConverterFactory, array $expected): public static function nameConverterProvider(): iterable { $expected = [ - 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', - 'title' => 'An error occurred', - 'detail' => "_d: a\n_4: 1", - 'violations' => [ - [ - 'propertyPath' => '_d', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - 'payload' => [ - 'severity' => 'warning', - ], - ], - [ - 'propertyPath' => '_4', - 'message' => '1', - 'code' => null, + [ + 'propertyPath' => '_d', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + 'payload' => [ + 'severity' => 'warning', ], ], + [ + 'propertyPath' => '_4', + 'message' => '1', + 'code' => null, + ], ]; $nameConverterFactory = function (self $that): NameConverterInterface { @@ -112,24 +98,19 @@ public static function nameConverterProvider(): iterable yield [$nameConverterFactory, $expected]; $expected = [ - 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', - 'title' => 'An error occurred', - 'detail' => "d: a\n4: 1", - 'violations' => [ - [ - 'propertyPath' => 'd', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - 'payload' => [ - 'severity' => 'warning', - ], - ], - [ - 'propertyPath' => '4', - 'message' => '1', - 'code' => null, + [ + 'propertyPath' => 'd', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + 'payload' => [ + 'severity' => 'warning', ], ], + [ + 'propertyPath' => '4', + 'message' => '1', + 'code' => null, + ], ]; yield [fn () => null, $expected]; } diff --git a/tests/Problem/Serializer/ErrorNormalizerTest.php b/tests/Problem/Serializer/ErrorNormalizerTest.php deleted file mode 100644 index 8eb62db291e..00000000000 --- a/tests/Problem/Serializer/ErrorNormalizerTest.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Problem\Serializer; - -use ApiPlatform\Problem\Serializer\ErrorNormalizer; -use ApiPlatform\State\ApiResource\Error; -use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Serializer\Serializer; - -/** - * @author Kévin Dunglas - */ -class ErrorNormalizerTest extends TestCase -{ - /** - * @group legacy - */ - public function testSupportNormalization(): void - { - $normalizer = new ErrorNormalizer(); - - $this->assertTrue($normalizer->supportsNormalization(new \Exception(), ErrorNormalizer::FORMAT)); - $this->assertFalse($normalizer->supportsNormalization(new \Exception(), 'xml')); - $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); - - $this->assertTrue($normalizer->supportsNormalization(new FlattenException(), ErrorNormalizer::FORMAT)); - $this->assertFalse($normalizer->supportsNormalization(new FlattenException(), 'xml')); - $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); - $this->assertSame([ - \Exception::class => true, - Error::class => false, - FlattenException::class => true, - ], $normalizer->getSupportedTypes($normalizer::FORMAT)); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } - } - - /** - * @group legacy - */ - public function testNormalize(): void - { - $normalizer = new ErrorNormalizer(); - - $this->assertEquals( - [ - 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', - 'title' => 'An error occurred', - 'detail' => 'Hello', - ], - $normalizer->normalize(new \Exception('Hello')) - ); - $this->assertEquals( - [ - 'type' => 'https://dunglas.fr', - 'title' => 'Hi', - 'detail' => 'Hello', - ], - $normalizer->normalize(new \Exception('Hello'), null, ['type' => 'https://dunglas.fr', 'title' => 'Hi']) - ); - } - - /** - * @dataProvider providerStatusCode - * - * @group legacy - * - * @param int $status http status code of the Exception - * @param string $originalMessage original message of the Exception - * @param bool $debug simulates kernel debug variable - */ - public function testErrorServerNormalize(int $status, string $originalMessage, bool $debug): void - { - $normalizer = new ErrorNormalizer($debug); - $exception = FlattenException::create(new \Exception($originalMessage), $status); - - $expected = [ - 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', - 'title' => 'An error occurred', - 'detail' => ($debug || $status < 500) ? $originalMessage : Response::$statusTexts[$status] ?? Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR], - ]; - - if ($debug) { - $expected['trace'] = $exception->getTrace(); - } - - $this->assertSame($expected, $normalizer->normalize($exception, null, ['statusCode' => $status])); - } - - /** - * @group legacy - */ - public static function providerStatusCode(): \Iterator - { - yield [Response::HTTP_INTERNAL_SERVER_ERROR, 'Sensitive SQL error displayed', false]; - yield [Response::HTTP_GATEWAY_TIMEOUT, 'Sensitive server error displayed', false]; - yield [Response::HTTP_BAD_REQUEST, 'Bad Request Message', false]; - yield [Response::HTTP_INTERNAL_SERVER_ERROR, 'Sensitive SQL error displayed', true]; - yield [Response::HTTP_GATEWAY_TIMEOUT, 'Sensitive server error displayed', true]; - yield [Response::HTTP_BAD_REQUEST, 'Bad Request Message', true]; - yield [509, Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR], true]; - } - - /** - * @group legacy - */ - public function testErrorServerNormalizeContextStatus(): void - { - $normalizer = new ErrorNormalizer(false); - $exception = FlattenException::create(new \Exception(''), 500); - - $expected = [ - 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', - 'title' => 'An error occurred', - 'detail' => Response::$statusTexts[502], - ]; - - $this->assertSame($expected, $normalizer->normalize($exception, null, ['statusCode' => 502])); - } -} diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php new file mode 100644 index 00000000000..cc6b9dfb53a --- /dev/null +++ b/tests/RecreateSchemaTrait.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Tools\SchemaTool; + +trait RecreateSchemaTrait +{ + /** + * @param class-string[] $classes + */ + private function recreateSchema(array $classes = []): void + { + $manager = $this->getManager(); + + if ($manager instanceof DocumentManager) { + $schemaManager = $manager->getSchemaManager(); + foreach ($classes as $c) { + $class = str_contains($c, 'Entity') ? str_replace('Entity', 'Document', $c) : $c; + $schemaManager->dropDocumentDatabase($class); + } + + return; + } + + /** @var ClassMetadata[] $cl */ + $cl = []; + foreach ($classes as $c) { + $cl[] = $manager->getMetadataFactory()->getMetadataFor($c); + } + + $schemaTool = new SchemaTool($manager); + @$schemaTool->dropSchema($cl); + @$schemaTool->createSchema($cl); + } + + private function isMongoDB(): bool + { + return 'mongodb' === static::getContainer()->getParameter('kernel.environment'); + } + + private function getManager(): EntityManagerInterface|DocumentManager + { + return static::getContainer()->get($this->isMongoDB() ? 'doctrine_mongodb' : 'doctrine')->getManager(); + } +} diff --git a/tests/SetupClassResourcesTrait.php b/tests/SetupClassResourcesTrait.php new file mode 100644 index 00000000000..0fdcc232511 --- /dev/null +++ b/tests/SetupClassResourcesTrait.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +use Symfony\Component\Routing\Router; + +trait SetupClassResourcesTrait +{ + use WithResourcesTrait; + + public static function setUpBeforeClass(): void + { + static::writeResources(self::getResources()); + } + + public static function tearDownAfterClass(): void + { + static::removeResources(); + $reflectionClass = new \ReflectionClass(Router::class); + $reflectionClass->setStaticPropertyValue('cache', []); + } + + /** + * @return class-string[] + */ + abstract public static function getResources(): array; +} diff --git a/tests/State/CreateProviderTest.php b/tests/State/CreateProviderTest.php index b11efb6e66d..6499e393444 100644 --- a/tests/State/CreateProviderTest.php +++ b/tests/State/CreateProviderTest.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Tests\State; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Post; diff --git a/tests/State/ObjectProviderTest.php b/tests/State/ObjectProviderTest.php index c2f93f55e3a..cb6cb491ac2 100644 --- a/tests/State/ObjectProviderTest.php +++ b/tests/State/ObjectProviderTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\State; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Post; use ApiPlatform\State\ObjectProvider; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyResourceWithComplexConstructor; diff --git a/tests/State/Pagination/ArrayPaginatorTest.php b/tests/State/Pagination/ArrayPaginatorTest.php index d172dd7faac..317ad6055e0 100644 --- a/tests/State/Pagination/ArrayPaginatorTest.php +++ b/tests/State/Pagination/ArrayPaginatorTest.php @@ -21,9 +21,7 @@ */ class ArrayPaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize(array $results, $firstResult, $maxResults, $currentItems, $totalItems, $currentPage, $lastPage, $hasNextPage): void { $paginator = new ArrayPaginator($results, $firstResult, $maxResults); diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index 41751233f09..66c420ce5cf 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -18,9 +18,7 @@ class TraversablePaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize( array $results, float $currentPage, diff --git a/tests/State/Provider/BackedEnumProviderTest.php b/tests/State/Provider/BackedEnumProviderTest.php index 934744681cb..8926ef9f647 100644 --- a/tests/State/Provider/BackedEnumProviderTest.php +++ b/tests/State/Provider/BackedEnumProviderTest.php @@ -34,7 +34,7 @@ public static function provideCollection(): iterable yield 'String case enum' => [BackedEnumStringResource::class, BackedEnumStringResource::cases()]; } - /** @dataProvider provideCollection */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideCollection')] public function testProvideCollection(string $class, array $expected): void { $operation = new GetCollection(class: $class); @@ -48,7 +48,7 @@ public static function provideItem(): iterable yield 'String case enum' => [BackedEnumStringResource::class, 'yes', BackedEnumStringResource::Yes]; } - /** @dataProvider provideItem */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideItem')] public function testProvideItem(string $class, string|int $id, \BackedEnum $expected): void { $operation = new Get(class: $class); diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 49b423959f8..d6b1643e571 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -24,6 +24,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; use PHPUnit\Framework\TestCase; @@ -54,6 +55,7 @@ public function testBuild(): void $containerProphecy->addCompilerPass(Argument::type(TestClientPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(TestMercureHubPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(AuthenticatorManagerPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(SerializerMappingLoaderPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal()); diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index fcd9e21b610..786a9c584f8 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -19,7 +19,7 @@ use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\Symfony\Bundle\ArgumentResolver\PayloadArgumentResolver; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ResourceImplementation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ResourceInterface; @@ -75,9 +75,7 @@ public function testItSupportsRequestWithDtoAsInput(): void $this->assertTrue($resolver->supports($request, $argument)); } - /** - * @dataProvider provideUnsupportedArguments - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideUnsupportedArguments')] public function testItDoesNotSupportArgumentThatCannotBeResolved(ArgumentMetadata $argument): void { $resolver = $this->createArgumentResolver(); @@ -91,9 +89,7 @@ public function testItDoesNotSupportArgumentThatCannotBeResolved(ArgumentMetadat $this->assertFalse($resolver->supports($request, $argument)); } - /** - * @dataProvider provideUnsupportedRequests - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideUnsupportedRequests')] public function testItDoesNotSupportRequestWithoutPayloadOfExpectedType(Request $request): void { $resolver = $this->createArgumentResolver(); @@ -183,9 +179,7 @@ public static function provideUnsupportedArguments(): iterable yield 'variadic argument' => [self::createArgumentMetadata(ResourceImplementation::class, true)]; } - /** - * @dataProvider provideIntegrationCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideIntegrationCases')] public function testIntegration(Request $request, callable $controller, array $expectedArguments): void { self::bootKernel(); diff --git a/tests/Symfony/Bundle/Command/DebugResourceCommandTest.php b/tests/Symfony/Bundle/Command/DebugResourceCommandTest.php index ad5425669f1..176afb9db5d 100644 --- a/tests/Symfony/Bundle/Command/DebugResourceCommandTest.php +++ b/tests/Symfony/Bundle/Command/DebugResourceCommandTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\Symfony\Bundle\Command; -use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException; use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Symfony\Bundle\Command\DebugResourceCommand; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AlternateResource; diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php deleted file mode 100644 index fb6ae42f95d..00000000000 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ /dev/null @@ -1,1323 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\Bundle\DependencyInjection; - -use ApiPlatform\Action\NotFoundAction; -use ApiPlatform\Doctrine\Common\State\PersistProcessor; -use ApiPlatform\Doctrine\Common\State\RemoveProcessor; -use ApiPlatform\Doctrine\Odm\Extension\AggregationCollectionExtensionInterface; -use ApiPlatform\Doctrine\Odm\Extension\AggregationItemExtensionInterface; -use ApiPlatform\Doctrine\Odm\State\CollectionProvider as MongoDbCollectionProvider; -use ApiPlatform\Doctrine\Odm\State\ItemProvider as MongoDbItemProvider; -use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface as DoctrineQueryCollectionExtensionInterface; -use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface; -use ApiPlatform\Doctrine\Orm\State\CollectionProvider; -use ApiPlatform\Doctrine\Orm\State\ItemProvider; -use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface; -use ApiPlatform\Elasticsearch\Filter\MatchFilter; -use ApiPlatform\Elasticsearch\Filter\TermFilter; -use ApiPlatform\Exception\ExceptionInterface; -use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\GraphQl\Error\ErrorHandlerInterface; -use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; -use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; -use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface as GraphQlSerializerContextBuilderInterface; -use ApiPlatform\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; -use ApiPlatform\JsonSchema\TypeFactoryInterface; -use ApiPlatform\Metadata\FilterInterface; -use ApiPlatform\Metadata\IdentifiersExtractorInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; -use ApiPlatform\OpenApi\Options; -use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; -use ApiPlatform\Serializer\Filter\GroupFilter; -use ApiPlatform\Serializer\Filter\PropertyFilter; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginationOptions; -use ApiPlatform\Symfony\Bundle\DependencyInjection\ApiPlatformExtension; -use ApiPlatform\Symfony\Messenger\Processor as MessengerProcessor; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface; -use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; -use ApiPlatform\Tests\Fixtures\TestBundle\TestBundle; -use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\ORM\OptimisticLockException; -use phpDocumentor\Reflection\DocBlockFactoryInterface; -use PHPStan\PhpDocParser\Parser\PhpDocParser; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\TwigBundle\TwigBundle; -use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Uid\AbstractUid; -use Symfony\Component\Validator\Validator\ValidatorInterface; - -/** - * The target configuration for API Platform 4 is at src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php - * this holds tests for the legacy configuration having event_listeners_backward_compatibility_layer=true. - * - * @group legacy - */ -class ApiPlatformExtensionTest extends TestCase -{ - use ExpectDeprecationTrait; - use ProphecyTrait; - - final public const DEFAULT_CONFIG = ['api_platform' => [ - 'title' => 'title', - 'description' => 'description', - 'version' => 'version', - 'formats' => [ - 'json' => ['mime_types' => ['json']], - 'jsonld' => ['mime_types' => ['application/ld+json']], - 'jsonhal' => ['mime_types' => ['application/hal+json']], - ], - 'http_cache' => ['invalidation' => [ - 'enabled' => true, - 'purger' => 'api_platform.http_cache.purger.varnish.ban', - 'request_options' => [ - 'allow_redirects' => [ - 'max' => 5, - 'protocols' => ['http', 'https'], - 'stric' => false, - 'referer' => false, - 'track_redirects' => false, - ], - 'http_errors' => true, - 'decode_content' => false, - 'verify' => false, - 'cookies' => true, - 'headers' => [ - 'User-Agent' => 'none', - ], - ], - ]], - 'doctrine_mongodb_odm' => [ - 'enabled' => true, - ], - 'defaults' => [ - 'extra_properties' => [], - 'url_generation_strategy' => UrlGeneratorInterface::ABS_URL, - ], - 'collection' => [ - 'exists_parameter_name' => 'exists', - 'order' => 'ASC', - 'order_parameter_name' => 'order', - 'order_nulls_comparison' => null, - 'pagination' => [ - 'page_parameter_name' => 'page', - 'enabled_parameter_name' => 'pagination', - 'items_per_page_parameter_name' => 'itemsPerPage', - 'partial_parameter_name' => 'partial', - ], - ], - 'error_formats' => [ - 'jsonproblem' => ['application/problem+json'], - 'jsonld' => ['application/ld+json'], - ], - 'patch_formats' => [], - 'exception_to_status' => [ - ExceptionInterface::class => Response::HTTP_BAD_REQUEST, - InvalidArgumentException::class => Response::HTTP_BAD_REQUEST, - FilterValidationException::class => Response::HTTP_BAD_REQUEST, - OptimisticLockException::class => Response::HTTP_CONFLICT, - ], - 'show_webby' => true, - 'eager_loading' => [ - 'enabled' => true, - 'max_joins' => 30, - 'force_eager' => true, - 'fetch_partial' => false, - ], - 'asset_package' => null, - 'enable_entrypoint' => true, - 'enable_docs' => true, - 'graphql' => [ - 'graphql_playground' => ['enabled' => false], - ], - 'keep_legacy_inflector' => false, - 'event_listeners_backward_compatibility_layer' => true, - ]]; - - private ContainerBuilder $container; - - protected function setUp(): void - { - $containerParameterBag = new ParameterBag([ - 'kernel.bundles' => [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - 'TwigBundle' => TwigBundle::class, - ], - 'kernel.bundles_metadata' => [ - 'TestBundle' => [ - 'parent' => null, - 'path' => realpath(__DIR__.'/../../../Fixtures/TestBundle'), - 'namespace' => TestBundle::class, - ], - ], - 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', - 'kernel.debug' => false, - 'kernel.environment' => 'test', - ]); - - $this->container = new ContainerBuilder($containerParameterBag); - } - - private function assertContainerHas(array $services, array $aliases = []): void - { - foreach ($services as $service) { - $this->assertTrue($this->container->hasDefinition($service), \sprintf('Definition "%s" not found.', $service)); - } - - foreach ($aliases as $alias) { - $this->assertContainerHasAlias($alias); - } - } - - private function assertNotContainerHasService(string $service): void - { - $this->assertFalse($this->container->hasDefinition($service), \sprintf('Service "%s" found.', $service)); - } - - private function assertContainerHasAlias(string $alias): void - { - $this->assertTrue($this->container->hasAlias($alias), \sprintf('Alias "%s" not found.', $alias)); - } - - private function assertServiceHasTags(string $service, array $tags = []): void - { - $serviceTags = $this->container->getDefinition($service)->getTags(); - - foreach ($tags as $tag) { - $this->assertArrayHasKey($tag, $serviceTags, \sprintf('Tag "%s" not found on the service "%s".', $tag, $service)); - } - } - - public function testCommonConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - 'api_platform.action.documentation', - 'api_platform.action.entrypoint', - 'api_platform.action.exception', - 'api_platform.action.not_found', - 'api_platform.action.placeholder', - 'api_platform.api.identifiers_extractor', - 'api_platform.filter_locator', - 'api_platform.negotiator', - 'api_platform.pagination', - 'api_platform.pagination_options', - 'api_platform.path_segment_name_generator.dash', - 'api_platform.path_segment_name_generator.underscore', - 'api_platform.ramsey_uuid.uri_variables.transformer.uuid', - 'api_platform.resource_class_resolver', - 'api_platform.route_loader', - 'api_platform.router', - 'api_platform.serializer.context_builder', - 'api_platform.serializer.context_builder.filter', - 'api_platform.serializer.group_filter', - 'api_platform.serializer.mapping.class_metadata_factory', - 'api_platform.serializer.normalizer.item', - 'api_platform.serializer.property_filter', - 'api_platform.serializer.uuid_denormalizer', - 'api_platform.serializer_locator', - 'api_platform.symfony.iri_converter', - 'api_platform.uri_variables.converter', - 'api_platform.uri_variables.transformer.date_time', - 'api_platform.uri_variables.transformer.integer', - - 'api_platform.state_provider.content_negotiation', - 'api_platform.state_provider.deserialize', - 'api_platform.state_processor.respond', - 'api_platform.state_processor.add_link_header', - 'api_platform.state_processor.serialize', - ]; - - $aliases = [ - NotFoundAction::class, - IdentifiersExtractorInterface::class, - IriConverterInterface::class, - ResourceClassResolverInterface::class, - UrlGeneratorInterface::class, - GroupFilter::class, - PropertyFilter::class, - SerializerContextBuilderInterface::class, - Pagination::class, - PaginationOptions::class, - 'api_platform.action.delete_item', - 'api_platform.action.get_collection', - 'api_platform.action.get_item', - 'api_platform.action.patch_item', - 'api_platform.action.post_collection', - 'api_platform.action.put_item', - 'api_platform.identifiers_extractor', - 'api_platform.iri_converter', - 'api_platform.path_segment_name_generator', - 'api_platform.property_accessor', - 'api_platform.property_info', - 'api_platform.serializer', - ]; - - $this->assertContainerHas($services, $aliases); - - $this->assertServiceHasTags('api_platform.cache.route_name_resolver', ['cache.pool']); - $this->assertServiceHasTags('api_platform.serializer.normalizer.item', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.serializer_locator', ['container.service_locator']); - $this->assertServiceHasTags('api_platform.filter_locator', ['container.service_locator']); - - // ramsey_uuid.xml - $this->assertServiceHasTags('api_platform.serializer.uuid_denormalizer', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.ramsey_uuid.uri_variables.transformer.uuid', ['api_platform.uri_variables.transformer']); - - // api.xml - $this->assertServiceHasTags('api_platform.route_loader', ['routing.loader']); - $this->assertServiceHasTags('api_platform.uri_variables.transformer.integer', ['api_platform.uri_variables.transformer']); - $this->assertServiceHasTags('api_platform.uri_variables.transformer.date_time', ['api_platform.uri_variables.transformer']); - } - - public function testCommonConfigurationAbstractUid(): void - { - if (!class_exists(AbstractUid::class)) { - $this->markTestSkipped('class Symfony\Component\Uid\AbstractUid does not exist'); - } - - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - 'api_platform.symfony.uri_variables.transformer.ulid', - 'api_platform.symfony.uri_variables.transformer.uuid', - ]; - - $this->assertContainerHas($services, []); - - $this->assertServiceHasTags('api_platform.symfony.uri_variables.transformer.ulid', ['api_platform.uri_variables.transformer']); - $this->assertServiceHasTags('api_platform.symfony.uri_variables.transformer.uuid', ['api_platform.uri_variables.transformer']); - } - - public static function dataProviderCommonConfigurationAliasNameConverter(): \Iterator - { - yield ['dummyValue', true]; - yield [null, false]; - } - - /** - * @dataProvider dataProviderCommonConfigurationAliasNameConverter - */ - public function testCommonConfigurationAliasNameConverter(?string $nameConverterConfig, bool $aliasIsExected): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['name_converter'] = $nameConverterConfig; - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertSame($aliasIsExected, $this->container->hasAlias('api_platform.name_converter')); - } - - public function testMetadataConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // metadata/xml.xml - 'api_platform.metadata.resource_extractor.xml', - 'api_platform.metadata.property_extractor.xml', - - // metadata/property_name.xml - 'api_platform.metadata.property.name_collection_factory.property_info', - 'api_platform.metadata.property.name_collection_factory.cached', - 'api_platform.metadata.property.name_collection_factory.xml', - - // metadata/links.xml - 'api_platform.metadata.resource.link_factory', - - // metadata/property.xml - 'api_platform.metadata.property.metadata_factory.property_info', - 'api_platform.metadata.property.metadata_factory.attribute', - 'api_platform.metadata.property.metadata_factory.serializer', - 'api_platform.metadata.property.metadata_factory.cached', - 'api_platform.metadata.property.metadata_factory.default_property', - 'api_platform.metadata.property.metadata_factory.xml', - 'api_platform.cache.metadata.property', - - // metadata/property_name.xml - 'api_platform.metadata.property.name_collection_factory.property_info', - 'api_platform.metadata.property.name_collection_factory.cached', - 'api_platform.metadata.property.name_collection_factory.xml', - - // metadata/resource.xml - 'api_platform.metadata.resource.metadata_collection_factory.attributes', - 'api_platform.metadata.resource.metadata_collection_factory.xml', - 'api_platform.metadata.resource.metadata_collection_factory.uri_template', - 'api_platform.metadata.resource.metadata_collection_factory.link', - 'api_platform.metadata.resource.metadata_collection_factory.operation_name', - 'api_platform.metadata.resource.metadata_collection_factory.input_output', - 'api_platform.metadata.resource.metadata_collection_factory.formats', - 'api_platform.metadata.resource.metadata_collection_factory.filters', - 'api_platform.metadata.resource.metadata_collection_factory.alternate_uri', - 'api_platform.metadata.resource.metadata_collection_factory.cached', - 'api_platform.cache.metadata.resource_collection', - - // metadata/resource_name.xml - 'api_platform.cache.metadata.resource', - 'api_platform.metadata.resource.name_collection_factory.attributes', - 'api_platform.metadata.resource.name_collection_factory.cached', - 'api_platform.metadata.resource.name_collection_factory.xml', - - // metadata/yaml.xml - 'api_platform.metadata.resource_extractor.yaml', - 'api_platform.metadata.property_extractor.yaml', - 'api_platform.metadata.resource.name_collection_factory.yaml', - 'api_platform.metadata.resource.metadata_collection_factory.yaml', - 'api_platform.metadata.property.metadata_factory.yaml', - 'api_platform.metadata.property.name_collection_factory.yaml', - - // metadata/operation.xml - 'api_platform.metadata.operation.metadata_factory', - ]; - - $aliases = [ - // metadata/property_name.xml - 'api_platform.metadata.property.name_collection_factory', - PropertyNameCollectionFactoryInterface::class, - - // metadata/property.xml - 'api_platform.metadata.property.metadata_factory', - - // metadata/resource.xml - 'api_platform.metadata.resource.metadata_collection_factory', - ResourceMetadataCollectionFactoryInterface::class, - - // metadata/resource_name.xml - 'api_platform.metadata.resource.name_collection_factory', - ResourceNameCollectionFactoryInterface::class, - ]; - - $this->assertContainerHas($services, $aliases); - - // metadata/property.xml - $this->assertServiceHasTags('api_platform.cache.metadata.property', ['cache.pool']); - - // metadata/resource.xml - $this->assertServiceHasTags('api_platform.cache.metadata.resource_collection', ['cache.pool']); - - // metadata/resource_name.xml - $this->assertServiceHasTags('api_platform.cache.metadata.resource', ['cache.pool']); - } - - public function testMetadataConfigurationDocBlockFactoryInterface(): void - { - if (!class_exists(PhpDocParser::class) || !interface_exists(DocBlockFactoryInterface::class)) { - $this->markTestSkipped('class PHPStan\PhpDocParser\Parser\PhpDocParser or phpDocumentor\Reflection\DocBlockFactoryInterface does not exist'); - } - - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // metadata/php_doc.xml - 'api_platform.metadata.resource.metadata_collection_factory.php_doc', - ]; - - $this->assertContainerHas($services, []); - } - - public function testSwaggerConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['enable_swagger'] = true; - $config['api_platform']['enable_swagger_ui'] = true; - - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // json_schema.xml - 'api_platform.json_schema.type_factory', - 'api_platform.json_schema.schema_factory', - 'api_platform.json_schema.json_schema_generate_command', - - // openapi.xml - 'api_platform.openapi.normalizer', - 'api_platform.openapi.options', - 'api_platform.openapi.command', - 'api_platform.openapi.normalizer.api_gateway', - 'api_platform.openapi.factory', - - // swagger_ui.xml - 'api_platform.swagger.listener.ui', - 'api_platform.swagger_ui.context', - 'api_platform.swagger_ui.action', - ]; - - $aliases = [ - // json_schema.xml - TypeFactoryInterface::class, - SchemaFactoryInterface::class, - - // openapi.xml - OpenApiNormalizer::class, - Options::class, - OpenApiFactoryInterface::class, - - // swagger_ui.xml - 'api_platform.swagger_ui.listener', - ]; - - $this->assertContainerHas($services, $aliases); - - // json_schema.xml - $this->assertServiceHasTags('api_platform.json_schema.json_schema_generate_command', ['console.command']); - - // openapi.xml - $this->assertServiceHasTags('api_platform.openapi.normalizer', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.openapi.command', ['console.command']); - $this->assertServiceHasTags('api_platform.openapi.normalizer.api_gateway', ['serializer.normalizer']); - - // swagger_ui.xml - $this->assertServiceHasTags('api_platform.swagger.listener.ui', ['kernel.event_listener']); - } - - public function testSwaggerUiDisabled(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['enable_swagger_ui'] = false; - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertNotContainerHasService('api_platform.swagger_ui.provider'); - } - - public function testJsonApiConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['formats']['jsonapi'] = [ - 'mime_types' => ['application/vnd.api+json'], - ]; - - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // jsonapi.xml - 'api_platform.jsonapi.encoder', - 'api_platform.jsonapi.name_converter.reserved_attribute_name', - 'api_platform.jsonapi.normalizer.entrypoint', - 'api_platform.jsonapi.normalizer.collection', - 'api_platform.jsonapi.normalizer.item', - 'api_platform.jsonapi.normalizer.object', - 'api_platform.jsonapi.normalizer.constraint_violation_list', - 'api_platform.jsonapi.normalizer.error', - 'api_platform.jsonapi.listener.request.transform_pagination_parameters', - 'api_platform.jsonapi.listener.request.transform_sorting_parameters', - 'api_platform.jsonapi.listener.request.transform_fieldsets_parameters', - 'api_platform.jsonapi.listener.request.transform_filtering_parameters', - ]; - - $this->assertContainerHas($services, []); - - // jsonapi.xml - $this->assertServiceHasTags('api_platform.jsonapi.encoder', ['serializer.encoder']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.entrypoint', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.collection', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.item', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.object', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.constraint_violation_list', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.normalizer.error', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonapi.listener.request.transform_pagination_parameters', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.jsonapi.listener.request.transform_sorting_parameters', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.jsonapi.listener.request.transform_fieldsets_parameters', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.jsonapi.listener.request.transform_filtering_parameters', ['kernel.event_listener']); - } - - public function testJsonLdHydraConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // jsonld.xml - 'api_platform.jsonld.context_builder', - 'api_platform.jsonld.normalizer.item', - 'api_platform.jsonld.normalizer.object', - 'api_platform.jsonld.encoder', - 'api_platform.jsonld.action.context', - - // hydra.xml - 'api_platform.hydra.normalizer.documentation', - 'api_platform.hydra.listener.response.add_link_header', - 'api_platform.hydra.normalizer.constraint_violation_list', - 'api_platform.hydra.normalizer.entrypoint', - 'api_platform.hydra.normalizer.error', - 'api_platform.hydra.normalizer.collection', - 'api_platform.hydra.normalizer.partial_collection_view', - 'api_platform.hydra.normalizer.collection_filters', - 'api_platform.hydra.json_schema.schema_factory', - ]; - - $this->assertContainerHas($services, []); - - // jsonld.xml - $this->assertServiceHasTags('api_platform.jsonld.normalizer.item', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonld.normalizer.object', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.jsonld.encoder', ['serializer.encoder']); - - // hydra.xml - $this->assertServiceHasTags('api_platform.hydra.normalizer.documentation', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hydra.listener.response.add_link_header', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.hydra.normalizer.constraint_violation_list', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hydra.normalizer.entrypoint', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hydra.normalizer.error', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hydra.normalizer.collection', ['serializer.normalizer']); - } - - public function testJsonHalConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // hal.xml - 'api_platform.hal.encoder', - 'api_platform.hal.normalizer.entrypoint', - 'api_platform.hal.normalizer.collection', - 'api_platform.hal.normalizer.item', - 'api_platform.hal.normalizer.object', - 'api_platform.hal.json_schema.schema_factory', - ]; - - $this->assertContainerHas($services, []); - - // hal.xml - $this->assertServiceHasTags('api_platform.hal.encoder', ['serializer.encoder']); - $this->assertServiceHasTags('api_platform.hal.normalizer.entrypoint', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hal.normalizer.collection', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hal.normalizer.item', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.hal.normalizer.object', ['serializer.normalizer']); - } - - public function testJsonProblemConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // problem.xml - 'api_platform.problem.encoder', - ]; - - $this->assertContainerHas($services, []); - - // problem.xml - $this->assertServiceHasTags('api_platform.problem.encoder', ['serializer.encoder']); - } - - public function testGraphQlConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $this->container->setParameter('kernel.debug', true); - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // graphql.xml - 'api_platform.graphql.executor', - 'api_platform.graphql.resolver_locator', - 'api_platform.graphql.iterable_type', - 'api_platform.graphql.upload_type', - 'api_platform.graphql.type_locator', - 'api_platform.graphql.types_container', - 'api_platform.graphql.types_factory', - 'api_platform.graphql.fields_builder_locator', - 'api_platform.graphql.action.entrypoint', - 'api_platform.graphql.action.graphiql', - 'api_platform.graphql.action.graphql_playground', - 'api_platform.graphql.error_handler', - 'api_platform.graphql.subscription.subscription_identifier_generator', - 'api_platform.graphql.cache.subscription', - 'api_platform.graphql.command.export_command', - 'api_platform.graphql.resolver.stage.write', - 'api_platform.graphql.resolver.stage.read', - 'api_platform.graphql.type_converter', - 'api_platform.graphql.type_builder', - 'api_platform.graphql.fields_builder', - 'api_platform.graphql.schema_builder', - 'api_platform.graphql.serializer.context_builder', - 'api_platform.graphql.resolver.factory.item', - 'api_platform.graphql.resolver.factory.collection', - 'api_platform.graphql.resolver.factory.item_mutation', - 'api_platform.graphql.resolver.factory.item_subscription', - 'api_platform.graphql.resolver.stage.security', - 'api_platform.graphql.resolver.stage.security_post_denormalize', - 'api_platform.graphql.resolver.stage.security_post_validation', - 'api_platform.graphql.resolver.stage.serialize', - 'api_platform.graphql.resolver.stage.deserialize', - 'api_platform.graphql.resolver.stage.validate', - 'api_platform.graphql.resolver.resource_field', - 'api_platform.graphql.normalizer.item', - 'api_platform.graphql.normalizer.object', - 'api_platform.graphql.subscription.subscription_manager', - 'api_platform.graphql.normalizer.error', - 'api_platform.graphql.normalizer.validation_exception', - 'api_platform.graphql.normalizer.http_exception', - 'api_platform.graphql.normalizer.runtime_exception', - 'api_platform.graphql.data_collector.resolver.factory.collection', - 'api_platform.graphql.data_collector.resolver.factory.item', - 'api_platform.graphql.data_collector.resolver.factory.item_mutation', - 'api_platform.graphql.data_collector.resolver.factory.item_subscription', - ]; - - $aliases = [ - // graphql.xml - GraphQlSerializerContextBuilderInterface::class, - ]; - - $this->assertContainerHas($services, $aliases); - - // graphql.xml - $this->assertServiceHasTags('api_platform.graphql.resolver_locator', ['container.service_locator']); - $this->assertServiceHasTags('api_platform.graphql.iterable_type', ['api_platform.graphql.type']); - $this->assertServiceHasTags('api_platform.graphql.upload_type', ['api_platform.graphql.type']); - $this->assertServiceHasTags('api_platform.graphql.type_locator', ['container.service_locator']); - $this->assertServiceHasTags('api_platform.graphql.fields_builder_locator', ['container.service_locator']); - $this->assertServiceHasTags('api_platform.graphql.cache.subscription', ['cache.pool']); - $this->assertServiceHasTags('api_platform.graphql.command.export_command', ['console.command']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.item', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.object', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.error', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.validation_exception', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.http_exception', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.graphql.normalizer.runtime_exception', ['serializer.normalizer']); - } - - public function testRuntimeExceptionIsThrownIfTwigIsNotEnabledButGraphqlClientsAre(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the test environnement or disable GraphiQL and GraphQL Playground.'); - - (new ApiPlatformExtension())->load($config, $this->container); - } - - public function testGraphqlClientsDefinitionsAreRemovedIfDisabled(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $config['api_platform']['graphql']['graphiql']['enabled'] = false; - $config['api_platform']['graphql']['graphql_playground']['enabled'] = false; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertNotContainerHasService('api_platform.graphql.action.graphiql'); - $this->assertNotContainerHasService('api_platform.graphql.action.graphql_playground'); - } - - public function testDoctrineOrmConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['doctrine']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // doctrine_orm.xml - 'api_platform.doctrine.metadata_factory', - 'api_platform.doctrine.orm.state.remove_processor', - 'api_platform.doctrine.orm.state.persist_processor', - 'api_platform.doctrine.orm.state.collection_provider', - 'api_platform.doctrine.orm.state.item_provider', - 'api_platform.doctrine.orm.search_filter', - 'api_platform.doctrine.orm.order_filter', - 'api_platform.doctrine.orm.range_filter', - 'api_platform.doctrine.orm.query_extension.eager_loading', - 'api_platform.doctrine.orm.query_extension.filter', - 'api_platform.doctrine.orm.query_extension.filter_eager_loading', - 'api_platform.doctrine.orm.query_extension.pagination', - 'api_platform.doctrine.orm.query_extension.order', - ]; - - $aliases = [ - // doctrine_orm.xml - RemoveProcessor::class, - PersistProcessor::class, - CollectionProvider::class, - ItemProvider::class, - 'ApiPlatform\Doctrine\Orm\Filter\OrderFilter', - 'ApiPlatform\Doctrine\Orm\Filter\RangeFilter', - 'ApiPlatform\Doctrine\Orm\Filter\DateFilter', - 'ApiPlatform\Doctrine\Orm\Filter\BooleanFilter', - 'ApiPlatform\Doctrine\Orm\Filter\NumericFilter', - 'ApiPlatform\Doctrine\Orm\Filter\ExistsFilter', - 'ApiPlatform\Doctrine\Orm\Extension\EagerLoadingExtension', - 'ApiPlatform\Doctrine\Orm\Extension\FilterExtension', - 'ApiPlatform\Doctrine\Orm\Extension\FilterEagerLoadingExtension', - 'ApiPlatform\Doctrine\Orm\Extension\PaginationExtension', - 'ApiPlatform\Doctrine\Orm\Extension\OrderExtension', - ]; - - $this->assertContainerHas($services, $aliases); - - // doctrine_orm.xml - $this->assertServiceHasTags('api_platform.doctrine.orm.state.remove_processor', ['api_platform.state_processor']); - $this->assertServiceHasTags('api_platform.doctrine.orm.state.persist_processor', ['api_platform.state_processor']); - $this->assertServiceHasTags('api_platform.doctrine.orm.state.collection_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.doctrine.orm.state.item_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.doctrine.orm.query_extension.eager_loading', ['api_platform.doctrine.orm.query_extension.item', 'api_platform.doctrine.orm.query_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine.orm.query_extension.filter', ['api_platform.doctrine.orm.query_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine.orm.query_extension.filter_eager_loading', ['api_platform.doctrine.orm.query_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine.orm.query_extension.pagination', ['api_platform.doctrine.orm.query_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine.orm.query_extension.order', ['api_platform.doctrine.orm.query_extension.collection']); - } - - public function testDoctrineMongoDbOdmConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['doctrine_mongodb_odm']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // doctrine_mongodb_odm.xml - 'api_platform.doctrine_mongodb.odm.default_document_manager.property_info_extractor', - 'api_platform.doctrine.metadata_factory', - 'api_platform.doctrine_mongodb.odm.state.remove_processor', - 'api_platform.doctrine_mongodb.odm.state.persist_processor', - 'api_platform.doctrine_mongodb.odm.state.collection_provider', - 'api_platform.doctrine_mongodb.odm.state.item_provider', - 'api_platform.doctrine_mongodb.odm.search_filter', - 'api_platform.doctrine_mongodb.odm.boolean_filter', - 'api_platform.doctrine_mongodb.odm.date_filter', - 'api_platform.doctrine_mongodb.odm.exists_filter', - 'api_platform.doctrine_mongodb.odm.numeric_filter', - 'api_platform.doctrine_mongodb.odm.order_filter', - 'api_platform.doctrine_mongodb.odm.range_filter', - 'api_platform.doctrine_mongodb.odm.metadata.property.metadata_factory', - 'api_platform.doctrine_mongodb.odm.aggregation_extension.filter', - 'api_platform.doctrine_mongodb.odm.aggregation_extension.pagination', - 'api_platform.doctrine_mongodb.odm.aggregation_extension.order', - ]; - - $aliases = [ - // doctrine_mongodb_odm.xml - RemoveProcessor::class, - PersistProcessor::class, - MongoDbCollectionProvider::class, - MongoDbItemProvider::class, - 'ApiPlatform\Doctrine\Odm\Filter\SearchFilter', - 'ApiPlatform\Doctrine\Odm\Filter\BooleanFilter', - 'ApiPlatform\Doctrine\Odm\Filter\DateFilter', - 'ApiPlatform\Doctrine\Odm\Filter\ExistsFilter', - 'ApiPlatform\Doctrine\Odm\Filter\NumericFilter', - 'ApiPlatform\Doctrine\Odm\Filter\OrderFilter', - 'ApiPlatform\Doctrine\Odm\Filter\RangeFilter', - 'ApiPlatform\Doctrine\Odm\Extension\FilterExtension', - 'ApiPlatform\Doctrine\Odm\Extension\PaginationExtension', - 'ApiPlatform\Doctrine\Odm\Extension\OrderExtension', - ]; - - $this->assertContainerHas($services, $aliases); - - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.state.remove_processor', ['api_platform.state_processor']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.state.persist_processor', ['api_platform.state_processor']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.state.collection_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.state.item_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.default_document_manager.property_info_extractor', ['property_info.list_extractor', 'property_info.type_extractor']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.aggregation_extension.filter', ['api_platform.doctrine_mongodb.odm.aggregation_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.aggregation_extension.pagination', ['api_platform.doctrine_mongodb.odm.aggregation_extension.collection']); - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.aggregation_extension.order', ['api_platform.doctrine_mongodb.odm.aggregation_extension.collection']); - } - - public function testHttpCacheConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['doctrine']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // http_cache.xml - 'api_platform.http_cache.listener.response.configure', - - // doctrine_orm_http_cache_purger.xml - 'api_platform.doctrine.listener.http_cache.purge', - - // http_cache_tags.xml - 'api_platform.http_cache.purger.varnish.xkey', - 'api_platform.http_cache.purger.varnish.ban', - 'api_platform.http_cache.listener.response.add_tags', - ]; - - $this->assertContainerHas($services); - - // http_cache.xml - $this->assertServiceHasTags('api_platform.http_cache.listener.response.configure', ['kernel.event_listener']); - - // doctrine_orm_http_cache_purger.xml - $this->assertServiceHasTags('api_platform.doctrine.listener.http_cache.purge', ['doctrine.event_listener']); - - // http_cache_tags.xml - $this->assertServiceHasTags('api_platform.http_cache.listener.response.add_tags', ['kernel.event_listener']); - - $this->assertContainerHasAlias('api_platform.http_cache.purger.varnish'); - - $this->assertEquals([ - ['event' => 'preUpdate'], - ['event' => 'onFlush'], - ['event' => 'postFlush'], - ], $this->container->getDefinition('api_platform.doctrine.listener.http_cache.purge')->getTag('doctrine.event_listener')); - } - - public function testValidatorConfiguration(): void - { - if (!interface_exists(ValidatorInterface::class)) { - $this->markTestSkipped('interface Symfony\Component\Validator\Validator\ValidatorInterface does not exist'); - } - - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // metadata/validator.xml - 'api_platform.metadata.property.metadata_factory.validator', - 'api_platform.metadata.property_schema.choice_restriction', - 'api_platform.metadata.property_schema.collection_restriction', - 'api_platform.metadata.property_schema.count_restriction', - 'api_platform.metadata.property_schema.greater_than_or_equal_restriction', - 'api_platform.metadata.property_schema.greater_than_restriction', - 'api_platform.metadata.property_schema.length_restriction', - 'api_platform.metadata.property_schema.less_than_or_equal_restriction', - 'api_platform.metadata.property_schema.less_than_restriction', - 'api_platform.metadata.property_schema.one_of_restriction', - 'api_platform.metadata.property_schema.range_restriction', - 'api_platform.metadata.property_schema.regex_restriction', - 'api_platform.metadata.property_schema.format_restriction', - 'api_platform.metadata.property_schema.unique_restriction', - - // symfony/validator.xml - 'api_platform.validator', - 'api_platform.listener.view.validate', - 'api_platform.validator.query_parameter_validator', - 'api_platform.listener.view.validate_query_parameters', - ]; - - $aliases = [ - // symfony/validator.xml - \ApiPlatform\Validator\ValidatorInterface::class, - ]; - - $this->assertContainerHas($services, $aliases); - - // metadata/validator.xml - $this->assertServiceHasTags('api_platform.metadata.property_schema.choice_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.collection_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.count_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.greater_than_or_equal_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.greater_than_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.length_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.less_than_or_equal_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.less_than_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.one_of_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.range_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.regex_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.format_restriction', ['api_platform.metadata.property_schema_restriction']); - $this->assertServiceHasTags('api_platform.metadata.property_schema.unique_restriction', ['api_platform.metadata.property_schema_restriction']); - - // symfony/validator.xml - $this->assertServiceHasTags('api_platform.listener.view.validate', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.listener.view.validate_query_parameters', ['kernel.event_listener']); - } - - public function testDataCollectorConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['enable_profiler'] = true; - $this->container->setParameter('kernel.debug', true); - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // data_collector.xml - 'api_platform.data_collector.request', - - // debug.xml - 'debug.var_dumper.cloner', - 'debug.var_dumper.cli_dumper', - 'debug.api_platform.debug_resource.command', - ]; - - $this->assertContainerHas($services, []); - - // data_collector.xml - $this->assertServiceHasTags('api_platform.data_collector.request', ['data_collector']); - - // debug.xml - $this->assertServiceHasTags('debug.api_platform.debug_resource.command', ['console.command']); - } - - public function testMercureConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['mercure']['enabled'] = true; - $config['api_platform']['doctrine']['enabled'] = true; - $config['api_platform']['doctrine_mongodb_odm']['enabled'] = true; - $config['api_platform']['graphql']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // mercure.xml - 'api_platform.mercure.listener.response.add_link_header', - - // doctrine_orm_mercure_publisher - 'api_platform.doctrine.orm.listener.mercure.publish', - - // doctrine_odm_mercure_publisher.xml - 'api_platform.doctrine_mongodb.odm.listener.mercure.publish', - - // graphql_mercure.xml - 'api_platform.graphql.subscription.mercure_iri_generator', - ]; - - $this->assertContainerHas($services, []); - - // mercure.xml - $this->assertServiceHasTags('api_platform.mercure.listener.response.add_link_header', ['kernel.event_listener']); - - // doctrine_orm_mercure_publisher - $this->assertServiceHasTags('api_platform.doctrine.orm.listener.mercure.publish', ['doctrine.event_listener']); - - // doctrine_odm_mercure_publisher.xml - $this->assertServiceHasTags('api_platform.doctrine_mongodb.odm.listener.mercure.publish', ['doctrine_mongodb.odm.event_listener']); - - $this->assertEquals([ - ['event' => 'onFlush'], - ['event' => 'postFlush'], - ], $this->container->getDefinition('api_platform.doctrine.orm.listener.mercure.publish')->getTag('doctrine.event_listener')); - - $this->assertEquals([ - ['event' => 'onFlush'], - ['event' => 'postFlush'], - ], $this->container->getDefinition('api_platform.doctrine_mongodb.odm.listener.mercure.publish')->getTag('doctrine_mongodb.odm.event_listener')); - } - - public function testMessengerConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // messenger.xml - MessengerProcessor::class, - ]; - - $aliases = [ - // messenger.xml - 'api_platform.message_bus', - ]; - - $this->assertContainerHas($services, $aliases); - - $this->assertServiceHasTags(MessengerProcessor::class, ['api_platform.state_processor']); - } - - public function testElasticsearchConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['elasticsearch']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // elasticsearch.xml - 'api_platform.elasticsearch.state.item_provider', - 'api_platform.elasticsearch.state.collection_provider', - 'api_platform.elasticsearch.client', - 'api_platform.elasticsearch.name_converter.inner_fields', - 'api_platform.elasticsearch.normalizer.item', - 'api_platform.elasticsearch.normalizer.document', - 'api_platform.elasticsearch.request_body_search_extension.filter', - 'api_platform.elasticsearch.request_body_search_extension.constant_score_filter', - 'api_platform.elasticsearch.request_body_search_extension.sort_filter', - 'api_platform.elasticsearch.request_body_search_extension.sort', - 'api_platform.elasticsearch.search_filter', - 'api_platform.elasticsearch.term_filter', - 'api_platform.elasticsearch.match_filter', - 'api_platform.elasticsearch.order_filter', - ]; - - $aliases = [ - // elasticsearch.xml - TermFilter::class, - MatchFilter::class, - \ApiPlatform\Elasticsearch\Filter\OrderFilter::class, - \ApiPlatform\Elasticsearch\State\ItemProvider::class, - \ApiPlatform\Elasticsearch\State\CollectionProvider::class, - ]; - - $this->assertContainerHas($services, $aliases); - - // elasticsearch.xml - $this->assertServiceHasTags('api_platform.elasticsearch.normalizer.document', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.elasticsearch.state.item_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.elasticsearch.state.collection_provider', ['api_platform.state_provider']); - $this->assertServiceHasTags('api_platform.elasticsearch.request_body_search_extension.constant_score_filter', ['api_platform.elasticsearch.request_body_search_extension.collection']); - $this->assertServiceHasTags('api_platform.elasticsearch.request_body_search_extension.sort_filter', ['api_platform.elasticsearch.request_body_search_extension.collection']); - $this->assertServiceHasTags('api_platform.elasticsearch.request_body_search_extension.sort', ['api_platform.elasticsearch.request_body_search_extension.collection']); - - $autoconfiguredInstances = $this->container->getAutoconfiguredInstanceof(); - $this->assertArrayHasKey(RequestBodySearchCollectionExtensionInterface::class, $autoconfiguredInstances); - $this->assertArrayHasKey('api_platform.elasticsearch.request_body_search_extension.collection', $autoconfiguredInstances[RequestBodySearchCollectionExtensionInterface::class]->getTags()); - } - - public function testSecurityConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // security.xml - 'api_platform.security.resource_access_checker', - 'api_platform.security.listener.request.deny_access', - 'api_platform.security.expression_language_provider', - ]; - - $aliases = [ - // security.xml - 'api_platform.security.expression_language', - ResourceAccessCheckerInterface::class, - ]; - - $this->assertContainerHas($services, $aliases); - - // security.xml - $this->assertServiceHasTags('api_platform.security.listener.request.deny_access', ['kernel.event_listener']); - $this->assertServiceHasTags('api_platform.security.expression_language_provider', ['security.expression_language_provider']); - - $this->assertEquals([ - ['event' => 'kernel.request', 'method' => 'onSecurity', 'priority' => 3], - ['event' => 'kernel.request', 'method' => 'onSecurityPostDenormalize', 'priority' => 1], - ['event' => 'kernel.view', 'method' => 'onSecurityPostValidation', 'priority' => 63], - ], $this->container->getDefinition('api_platform.security.listener.request.deny_access')->getTag('kernel.event_listener')); - } - - public function testMakerConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['maker']['enabled'] = true; - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertTrue($this->container->has('api_platform.maker.command.state_processor')); - $this->assertTrue($this->container->has('api_platform.maker.command.state_provider')); - } - - public function testArgumentResolverConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // argument_resolver.xml - 'api_platform.argument_resolver.payload', - ]; - - $this->assertContainerHas($services); - - // argument_resolver.xml - $this->assertServiceHasTags('api_platform.argument_resolver.payload', ['controller.argument_value_resolver']); - } - - public function testAutoConfigurableInterfaces(): void - { - $config = self::DEFAULT_CONFIG; - (new ApiPlatformExtension())->load($config, $this->container); - - $interfaces = [ - FilterInterface::class => 'api_platform.filter', - ValidationGroupsGeneratorInterface::class => 'api_platform.validation_groups_generator', - PropertySchemaRestrictionMetadataInterface::class => 'api_platform.metadata.property_schema_restriction', - QueryItemResolverInterface::class => 'api_platform.graphql.resolver', - QueryCollectionResolverInterface::class => 'api_platform.graphql.resolver', - MutationResolverInterface::class => 'api_platform.graphql.resolver', - GraphQlTypeInterface::class => 'api_platform.graphql.type', - ErrorHandlerInterface::class => 'api_platform.graphql.error_handler', - QueryItemExtensionInterface::class => 'api_platform.doctrine.orm.query_extension.item', - DoctrineQueryCollectionExtensionInterface::class => 'api_platform.doctrine.orm.query_extension.collection', - AggregationItemExtensionInterface::class => 'api_platform.doctrine_mongodb.odm.aggregation_extension.item', - AggregationCollectionExtensionInterface::class => 'api_platform.doctrine_mongodb.odm.aggregation_extension.collection', - ]; - - $has = []; - foreach ($this->container->getAutoconfiguredInstanceof() as $interface => $childDefinition) { - if (isset($interfaces[$interface])) { - $has[] = $interface; - $this->assertArrayHasKey($interfaces[$interface], $childDefinition->getTags()); - } - } - - $this->assertEmpty(array_diff(array_keys($interfaces), $has), 'Not all expected interfaces are autoconfigurable.'); - } - - public function testDefaults(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['defaults'] = [ - 'something' => 'test', - 'extra_properties' => ['else' => 'foo'], - ]; - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertEquals($this->container->getParameter('api_platform.defaults'), ['extra_properties' => ['else' => 'foo', 'something' => 'test']]); - } - - public function testConfigurationDirectories(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['defaults'] = [ - 'something' => 'test', - 'extra_properties' => ['else' => 'foo'], - ]; - - (new ApiPlatformExtension())->load($config, $this->container); - - $kernelProjectDir = realpath(__DIR__.'/../../../Fixtures/TestBundle'); - $resourceClassDirectories = $this->container->getParameter('api_platform.resource_class_directories'); - - $this->assertContains($kernelProjectDir.'/Resources/config/api_resources', $resourceClassDirectories); - $this->assertContains($kernelProjectDir.'/Entity', $resourceClassDirectories); - $this->assertContains($kernelProjectDir.'/Document', $resourceClassDirectories); - $this->assertContains(realpath(__DIR__.'/../../../Symfony/Bundle/DependencyInjection').'/../../../Fixtures/app/config/api_platform', $resourceClassDirectories); - } - - /** - * @group legacy - */ - public function testDeprecatedHttpCacheConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['http_cache']['invalidation']['varnish_urls'] = ['test']; - $config['api_platform']['http_cache']['invalidation']['xkey'] = ['glue' => ' ']; - - (new ApiPlatformExtension())->load($config, $this->container); - $this->assertServiceHasTags('api_platform.invalidation_http_client.0', ['api_platform.http_cache.http_client']); - } - - public function testHttpCacheUrlsConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['http_cache']['invalidation']['urls'] = ['test']; - $config['api_platform']['http_cache']['invalidation']['scoped_clients'] = ['my_scoped_client']; - - $this->container->setDefinition('my_scoped_client', new Definition('my_scoped_client')); - - (new ApiPlatformExtension())->load($config, $this->container); - $this->assertServiceHasTags('api_platform.invalidation_http_client.0', ['api_platform.http_cache.http_client']); - $this->assertServiceHasTags('my_scoped_client', ['api_platform.http_cache.http_client']); - } - - public function testHttpCacheBanConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - - (new ApiPlatformExtension())->load($config, $this->container); - - $service = $this->container->getDefinition('api_platform.http_cache.purger.varnish.ban'); - $this->assertCount(1, $service->getArguments()); - $this->assertEquals('api_platform.http_cache.http_client', $service->getArgument(0)->getTag()); - } - - public function testLegacyOpenApiApiKeysConfiguration(): void - { - $this->expectException(InvalidConfigurationException::class); - $config = self::DEFAULT_CONFIG; - $config['api_platform']['swagger']['api_keys']['Some Authorization Name'] = ['name' => 'a', 'type' => 'header']; - - (new ApiPlatformExtension())->load($config, $this->container); - } - - public function testHasClassMetadataCache(): void - { - $config = self::DEFAULT_CONFIG; - $this->container->setParameter('kernel.debug', true); - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertFalse($this->container->hasDefinition('api_platform.serializer.mapping.cache_class_metadata_factory')); - } - - /** - * @group legacy - */ - public function testLegacyGraphQlConfigurationWithoutJsonFormat(): void - { - $this->expectDeprecation('Since api-platform/core 3.2: Add the "json" format to the configuration to use GraphQL.'); - $config = self::DEFAULT_CONFIG; - unset($config['api_platform']['formats']['json']); - - (new ApiPlatformExtension())->load($config, $this->container); - $this->assertArrayHasKey('json', $this->container->getParameter('api_platform.formats')); - } - - /** - * @see https://github.com/api-platform/core/issues/5919 - */ - public function testGraphQlLegacyConfigurationInDebugMode(): void - { - $config = self::DEFAULT_CONFIG; - - (new ApiPlatformExtension())->load($config, $this->container); - $this->assertTrue($this->container->hasDefinition('api_platform.graphql.resolver.factory.item')); - } - - /** - * @group legacy - */ - public function testLegacyJsonProblemConfiguration(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['defaults']['extra_properties'] = ['rfc_7807_compliant_errors' => false]; - (new ApiPlatformExtension())->load($config, $this->container); - - $services = [ - // problem.xml - 'api_platform.problem.normalizer.constraint_violation_list', - 'api_platform.problem.normalizer.error', - ]; - - $this->assertContainerHas($services, []); - - // problem.xml - $this->assertServiceHasTags('api_platform.problem.normalizer.constraint_violation_list', ['serializer.normalizer']); - $this->assertServiceHasTags('api_platform.problem.normalizer.error', ['serializer.normalizer']); - } -} diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/GraphQlResolverPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/GraphQlResolverPassTest.php index e98d12adb83..27d1134bfba 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/GraphQlResolverPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/GraphQlResolverPassTest.php @@ -15,7 +15,6 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -25,8 +24,6 @@ */ class GraphQlResolverPassTest extends TestCase { - use ExpectDeprecationTrait; - public function testProcess(): void { $filterPass = new GraphQlResolverPass(); @@ -49,32 +46,4 @@ public function testProcess(): void $filterPass->process($containerBuilder); } - - /** - * @group legacy - */ - public function testProcessDeprecated(): void - { - $this->expectDeprecation('Since api-platform/core 3.2: The tag "api_platform.graphql.query_resolver" is deprecated use "api_platform.graphql.resolver" instead.'); - $this->expectDeprecation('Since api-platform/core 3.2: The tag "api_platform.graphql.mutation_resolver" is deprecated use "api_platform.graphql.resolver" instead.'); - $filterPass = new GraphQlResolverPass(); - - $this->assertInstanceOf(CompilerPassInterface::class, $filterPass); - - $typeLocatorDefinition = $this->createMock(Definition::class); - $typeLocatorDefinition->expects($this->once())->method('addArgument')->with($this->callback(function () { - return true; - })); - - $containerBuilder = $this->createMock(ContainerBuilder::class); - $containerBuilder->expects($this->once())->method('getParameter')->with('api_platform.graphql.enabled')->willReturn(true); - $containerBuilder->method('findTaggedServiceIds')->willReturnOnConsecutiveCalls( - ['a' => []], - ['b' => []], - [] - ); - $containerBuilder->method('getDefinition')->with('api_platform.graphql.resolver_locator')->willReturn($typeLocatorDefinition); - - $filterPass->process($containerBuilder); - } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index ba44d094e7c..84a1ed5066c 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -14,11 +14,9 @@ namespace ApiPlatform\Tests\Symfony\Bundle\DependencyInjection; use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface; use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration; use Doctrine\ORM\OptimisticLockException; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -33,8 +31,6 @@ */ class ConfigurationTest extends TestCase { - use ExpectDeprecationTrait; - private Configuration $configuration; private Processor $processor; @@ -50,9 +46,6 @@ public function testDefaultConfig(): void $this->runDefaultConfigTests(); } - /** - * @group mongodb - */ public function testDefaultConfigWithMongoDbOdm(): void { $this->runDefaultConfigTests(['orm', 'odm']); @@ -82,11 +75,12 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'description' => 'description', 'version' => '1.0.0', 'show_webby' => true, - 'formats' => [], + 'formats' => [ + 'jsonld' => ['mime_types' => ['application/ld+json']], + ], 'docs_formats' => [ 'jsonopenapi' => ['mime_types' => ['application/vnd.openapi+json']], 'yamlopenapi' => ['mime_types' => ['application/vnd.openapi+yaml']], - 'json' => ['mime_types' => ['application/json']], 'jsonld' => ['mime_types' => ['application/ld+json']], 'html' => ['mime_types' => ['text/html']], ], @@ -102,7 +96,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'exception_to_status' => [ ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST, - ValidationExceptionInterface::class => Response::HTTP_BAD_REQUEST, OptimisticLockException::class => Response::HTTP_CONFLICT, ], 'path_segment_name_generator' => 'api_platform.metadata.path_segment_name_generator.underscore', @@ -110,7 +103,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'validator' => [ 'serialize_payload_fields' => [], 'query_parameter_validation' => true, - 'legacy_validation_exception' => true, ], 'name_converter' => null, 'enable_swagger' => true, @@ -141,7 +133,6 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'elasticsearch' => [ 'enabled' => false, 'hosts' => [], - 'mapping' => [], ], 'oauth' => [ 'enabled' => false, @@ -228,10 +219,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'maker' => [ 'enabled' => true, ], - 'keep_legacy_inflector' => true, - 'event_listeners_backward_compatibility_layer' => null, 'use_symfony_listeners' => false, - 'use_deprecated_json_schema_type_factory' => null, 'handle_symfony_errors' => false, 'enable_link_security' => false, 'serializer' => [ @@ -250,9 +238,7 @@ public static function invalidHttpStatusCodeProvider(): array ]; } - /** - * @dataProvider invalidHttpStatusCodeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('invalidHttpStatusCodeProvider')] public function testExceptionToStatusConfigWithInvalidHttpStatusCode($invalidHttpStatusCode): void { $this->expectException(InvalidConfigurationException::class); @@ -279,9 +265,7 @@ public static function invalidHttpStatusCodeValueProvider(): array ]; } - /** - * @dataProvider invalidHttpStatusCodeValueProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('invalidHttpStatusCodeValueProvider')] public function testExceptionToStatusConfigWithInvalidHttpStatusCodeValue($invalidHttpStatusCodeValue): void { $this->expectException(InvalidTypeException::class); diff --git a/tests/Symfony/Bundle/EventListener/SwaggerUiListenerTest.php b/tests/Symfony/Bundle/EventListener/SwaggerUiListenerTest.php deleted file mode 100644 index 6387bcb46a8..00000000000 --- a/tests/Symfony/Bundle/EventListener/SwaggerUiListenerTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\Bundle\EventListener; - -use ApiPlatform\Symfony\Bundle\EventListener\SwaggerUiListener; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @author Kévin Dunglas - */ -class SwaggerUiListenerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider getParameters - */ - public function testOnKernelRequest(Request $request, ?string $controller = null): void - { - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $listener = new SwaggerUiListener(); - $listener->onKernelRequest($eventProphecy->reveal()); - - $this->assertSame($controller, $request->attributes->get('_controller')); - } - - public static function getParameters(): array - { - $respondRequest = new Request([], [], ['_api_respond' => true]); - $respondRequest->setRequestFormat('html'); - - $resourceClassRequest = new Request([], [], ['_api_resource_class' => 'Foo']); - $resourceClassRequest->setRequestFormat('html'); - - $jsonRequest = new Request([], [], ['_api_resource_class' => 'Foo']); - $jsonRequest->setRequestFormat('json'); - - return [ - [$respondRequest, 'api_platform.swagger_ui.action'], - [$resourceClassRequest, 'api_platform.swagger_ui.action'], - [new Request(), null], - [$jsonRequest, null], - ]; - } -} diff --git a/tests/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php b/tests/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php index 781e440dddb..a3659fd7057 100644 --- a/tests/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php +++ b/tests/Symfony/Bundle/SwaggerUi/SwaggerUiActionTest.php @@ -47,9 +47,7 @@ class SwaggerUiActionTest extends TestCase ], ]; - /** - * @dataProvider getInvokeParameters - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getInvokeParameters')] public function testInvoke(Request $request, callable $twigFactory): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -173,9 +171,7 @@ public static function getInvokeParameters(): iterable yield [new Request([], [], ['_api_resource_class' => 'Foo'], [], [], ['REQUEST_URI' => '/docs', 'SCRIPT_FILENAME' => '/docs']), $twigItemFactory]; } - /** - * @dataProvider getDoNotRunCurrentRequestParameters - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getDoNotRunCurrentRequestParameters')] public function testDoNotRunCurrentRequest(Request $request): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); diff --git a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php index f052ab4efc0..4039f0ed78b 100644 --- a/tests/Symfony/Bundle/Test/ApiTestCaseTest.php +++ b/tests/Symfony/Bundle/Test/ApiTestCaseTest.php @@ -14,30 +14,52 @@ namespace ApiPlatform\Tests\Symfony\Bundle\Test; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; -use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5921\ExceptionResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\OperationPriorities; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\DirectMercure as DirectMercureDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Address; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DirectMercure; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6041\NumericValidated; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6146\Issue6146Child; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6146\Issue6146Parent; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonSchemaContextDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User; use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Tools\SchemaTool; use PHPUnit\Framework\ExpectationFailedException; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; class ApiTestCaseTest extends ApiTestCase { - use ExpectDeprecationTrait; + use RecreateSchemaTrait; + use SetupClassResourcesTrait; - public static function providerFormats(): iterable + /** + * @return class-string[] + */ + public static function getResources(): array { - yield 'jsonapi' => ['jsonapi', 'application/vnd.api+json']; - yield 'jsonhal' => ['jsonhal', 'application/hal+json']; - yield 'jsonld' => ['jsonld', 'application/ld+json']; + return [ + Address::class, + DirectMercure::class, + Dummy::class, + DummyDtoInputOutput::class, + NumericValidated::class, + Issue6146Child::class, + Issue6146Parent::class, + JsonSchemaContextDummy::class, + RelatedDummy::class, + RelatedOwnedDummy::class, + User::class, + ResourceInterface::class, + OperationPriorities::class, + ExceptionResource::class, + ]; } public function testAssertJsonContains(): void @@ -117,21 +139,25 @@ public function testAssertMatchesJsonSchema(): void $this->assertMatchesJsonSchema(json_decode($jsonSchema, true)); } - /** - * @dataProvider providerFormats - */ + public static function providerFormats(): iterable + { + yield 'jsonapi' => ['jsonapi', 'application/vnd.api+json']; + yield 'jsonhal' => ['jsonhal', 'application/hal+json']; + yield 'jsonld' => ['jsonld', 'application/ld+json']; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] public function testAssertMatchesResourceCollectionJsonSchema(string $format, string $mimeType): void { self::createClient()->request('GET', '/resource_interfaces', ['headers' => ['Accept' => $mimeType]]); $this->assertMatchesResourceCollectionJsonSchema(ResourceInterface::class, format: $format); } - /** - * @dataProvider providerFormats - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testAssertMatchesResourceCollectionJsonSchemaKeepSerializationContext(string $format, string $mimeType): void { - $this->recreateSchema(); + $this->recreateSchema([Issue6146Parent::class, Issue6146Child::class]); /** @var EntityManagerInterface $manager */ $manager = static::getContainer()->get('doctrine')->getManager(); @@ -154,21 +180,18 @@ public function testAssertMatchesResourceCollectionJsonSchemaKeepSerializationCo $this->assertMatchesResourceCollectionJsonSchema(Issue6146Parent::class, format: $format); } - /** - * @dataProvider providerFormats - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] public function testAssertMatchesResourceItemJsonSchema(string $format, string $mimeType): void { self::createClient()->request('GET', '/resource_interfaces/some-id', ['headers' => ['Accept' => $mimeType]]); $this->assertMatchesResourceItemJsonSchema(ResourceInterface::class, format: $format); } - /** - * @dataProvider providerFormats - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testAssertMatchesResourceItemJsonSchemaWithCustomJson(string $format, string $mimeType): void { - $this->recreateSchema(); + $this->recreateSchema([JsonSchemaContextDummy::class]); /** @var EntityManagerInterface $manager */ $manager = static::getContainer()->get('doctrine')->getManager(); @@ -180,12 +203,11 @@ public function testAssertMatchesResourceItemJsonSchemaWithCustomJson(string $fo $this->assertMatchesResourceItemJsonSchema(JsonSchemaContextDummy::class, format: $format); } - /** - * @dataProvider providerFormats - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testAssertMatchesResourceItemJsonSchemaOutput(string $format, string $mimeType): void { - $this->recreateSchema(); + $this->recreateSchema([DummyDtoInputOutput::class]); /** @var EntityManagerInterface $manager */ $manager = static::getContainer()->get('doctrine')->getManager(); @@ -198,12 +220,11 @@ public function testAssertMatchesResourceItemJsonSchemaOutput(string $format, st $this->assertMatchesResourceItemJsonSchema(DummyDtoInputOutput::class, format: $format); } - /** - * @dataProvider providerFormats - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithContext(string $format, string $mimeType): void { - $this->recreateSchema(); + $this->recreateSchema([User::class]); /** @var EntityManagerInterface $manager */ $manager = static::getContainer()->get('doctrine')->getManager(); @@ -221,9 +242,10 @@ public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithCo $this->assertMatchesResourceCollectionJsonSchema(User::class, null, $format, ['groups' => ['api-test-case-group']]); } + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testAssertMatchesResourceItemAndCollectionJsonSchemaOutputWithRangeAssertions(): void { - $this->recreateSchema(); + $this->recreateSchema([NumericValidated::class]); /** @var EntityManagerInterface $manager */ $manager = static::getContainer()->get('doctrine')->getManager(); @@ -261,9 +283,10 @@ public function testAssertArraySubsetDoesNothingForValidScenario(): void $this->assertArraySubset([1, 2], [1, 2, 3]); } + #[\PHPUnit\Framework\Attributes\Group('orm')] public function testFindIriBy(): void { - $this->recreateSchema(); + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class]); self::createClient()->request('POST', '/dummies', [ 'headers' => [ @@ -274,10 +297,8 @@ public function testFindIriBy(): void ]); $this->assertResponseIsSuccessful(); - $container = static::getContainer(); - $resource = 'mongodb' === $container->getParameter('kernel.environment') ? DummyDocument::class : Dummy::class; - $this->assertMatchesRegularExpression('~^/dummies/\d+~', self::findIriBy($resource, ['name' => 'Kevin'])); - $this->assertNull(self::findIriBy($resource, ['name' => 'not-exist'])); + $this->assertMatchesRegularExpression('~^/dummies/\d+~', self::findIriBy(Dummy::class, ['name' => 'Kevin'])); + $this->assertNull(self::findIriBy(Dummy::class, ['name' => 'not-exist'])); } public function testGetPrioritizedOperation(): void @@ -290,12 +311,11 @@ public function testGetPrioritizedOperation(): void $this->assertResponseIsSuccessful(); } - /** - * @group mercure - */ + #[\PHPUnit\Framework\Attributes\Group('mercure')] public function testGetMercureMessages(): void { - $this->recreateSchema(['environment' => 'mercure']); + self::bootKernel(['environment' => 'mercure']); + $this->recreateSchema([$this->isMongoDB() ? DirectMercureDocument::class : DirectMercure::class]); self::createClient()->request('POST', '/direct_mercures', [ 'headers' => [ @@ -348,23 +368,6 @@ public function testGetMercureMessages(): void ); } - private function recreateSchema(array $options = []): void - { - self::bootKernel($options); - - /** @var EntityManagerInterface $manager */ - $manager = static::getContainer()->get('doctrine')->getManager(); - /** @var ClassMetadata[] $classes */ - $classes = $manager->getMetadataFactory()->getAllMetadata(); - $schemaTool = new SchemaTool($manager); - - @$schemaTool->dropSchema($classes); - @$schemaTool->createSchema($classes); - } - - /** - * @group legacy - */ public function testExceptionNormalizer(): void { $response = self::createClient()->request('GET', '/issue5921', [ @@ -380,7 +383,7 @@ public function testExceptionNormalizer(): void public function testMissingMethod(): void { - $response = self::createClient([], ['headers' => ['accept' => 'application/json']])->request('DELETE', '/something/that/does/not/exist/ever'); + self::createClient([], ['headers' => ['accept' => 'application/json']])->request('DELETE', '/something/that/does/not/exist/ever'); $this->assertResponseStatusCodeSame(404); } } diff --git a/tests/Symfony/Bundle/Test/ClientTest.php b/tests/Symfony/Bundle/Test/ClientTest.php index 8dc41114b2a..25e0466ce4c 100644 --- a/tests/Symfony/Bundle/Test/ClientTest.php +++ b/tests/Symfony/Bundle/Test/ClientTest.php @@ -15,27 +15,31 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use ApiPlatform\Symfony\Bundle\Test\Response; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Tools\SchemaTool; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\Security\Core\User\InMemoryUser; class ClientTest extends ApiTestCase { - protected function setUp(): void + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array { - self::bootKernel(); - /** - * @var EntityManagerInterface - */ - $manager = static::getContainer()->get('doctrine')->getManager(); - /** @var ClassMetadata[] $classes */ - $classes = $manager->getMetadataFactory()->getAllMetadata(); - $schemaTool = new SchemaTool($manager); - - @$schemaTool->dropSchema($classes); - @$schemaTool->createSchema($classes); + return [ + Dummy::class, + RelatedDummy::class, + RelatedOwnedDummy::class, + SecuredDummy::class, + ]; } public function testRequest(): void @@ -54,6 +58,7 @@ public function testRequest(): void public function testCustomHeader(): void { + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class]); $client = self::createClient(); $client->disableReboot(); $response = $client->request('POST', '/dummies', [ @@ -75,6 +80,7 @@ public function testCustomHeader(): void public function testDefaultHeaders(): void { + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class]); $client = self::createClient([], [ 'headers' => [ 'content-type' => 'application/json', @@ -92,11 +98,10 @@ public function testDefaultHeaders(): void $this->assertStringContainsString('Kevin', $response->getContent()); } - /** - * @dataProvider authBasicProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('authBasicProvider')] public function testAuthBasic($basic): void { + $this->recreateSchema([SecuredDummy::class, RelatedDummy::class]); $client = self::createClient(); $client->enableReboot(); $response = $client->request('GET', '/secured_dummies', ['auth_basic' => $basic]); @@ -112,6 +117,7 @@ public static function authBasicProvider(): iterable public function testComplexScenario(): void { + $this->recreateSchema([SecuredDummy::class, RelatedDummy::class]); self::createClient()->request('GET', '/secured_dummies', ['auth_basic' => ['dunglas', 'kevin']]); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); @@ -157,6 +163,7 @@ public function testStream(): void public function testLoginUser(): void { + $this->recreateSchema([SecuredDummy::class, RelatedDummy::class]); $client = self::createClient(); $client->loginUser(new InMemoryUser('dunglas', 'kevin', ['ROLE_USER'])); diff --git a/tests/Symfony/Bundle/Test/Constraint/ArraySubsetTest.php b/tests/Symfony/Bundle/Test/Constraint/ArraySubsetTest.php index 67974c3b9c9..f7dfaa0a24c 100644 --- a/tests/Symfony/Bundle/Test/Constraint/ArraySubsetTest.php +++ b/tests/Symfony/Bundle/Test/Constraint/ArraySubsetTest.php @@ -59,9 +59,7 @@ public static function evaluateDataProvider(): array ]; } - /** - * @dataProvider evaluateDataProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('evaluateDataProvider')] public function testEvaluate(bool $expected, iterable $subset, iterable $other, bool $strict): void { $constraint = new ArraySubset($subset, $strict); diff --git a/tests/Symfony/Bundle/Test/ResponseTest.php b/tests/Symfony/Bundle/Test/ResponseTest.php index 52e112859f4..a147ff9f42b 100644 --- a/tests/Symfony/Bundle/Test/ResponseTest.php +++ b/tests/Symfony/Bundle/Test/ResponseTest.php @@ -42,9 +42,7 @@ public function testCreate(): void $this->assertSame('', $response->getContent()); } - /** - * @dataProvider errorProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('errorProvider')] public function testCheckStatus(string $expectedException, int $status): void { $this->expectException($expectedException); diff --git a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php index f8e65642012..4056d3baf48 100644 --- a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php +++ b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php @@ -15,55 +15,32 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DocumentDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Tools\SchemaTool; -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** * @author Anthony GRASSIOT */ -class ApiPlatformProfilerPanelTest extends WebTestCase +final class ApiPlatformProfilerPanelTest extends WebTestCase { - use ExpectDeprecationTrait; - private EntityManagerInterface $manager; - private SchemaTool $schemaTool; - private string $env; + use RecreateSchemaTrait; + use SetupClassResourcesTrait; - protected function setUp(): void - { - $kernel = self::bootKernel(); - $this->env = $kernel->getEnvironment(); - - /** @var ManagerRegistry $doctrine */ - $doctrine = $kernel->getContainer()->get('doctrine'); - /** @var EntityManagerInterface $manager */ - $manager = $doctrine->getManager(); - $this->manager = $manager; - $this->schemaTool = new SchemaTool($this->manager); - /** @var ClassMetadata[] $classes */ - $classes = $this->manager->getMetadataFactory()->getAllMetadata(); - @$this->schemaTool->dropSchema($classes); - $this->manager->clear(); - @$this->schemaTool->createSchema($classes); - - $this->ensureKernelShutdown(); - } - - protected function tearDown(): void + /** + * @return class-string[] + */ + public static function getResources(): array { - $this->schemaTool->dropSchema($this->manager->getMetadataFactory()->getAllMetadata()); - $this->manager->clear(); - parent::tearDown(); + return [ + Dummy::class, + RelatedDummy::class, + RelatedOwnedDummy::class, + ]; } - /** - * TODO: remove openapiContext to get rid of the legacy. - * - * @group legacy - */ public function testDebugBarContentNotResourceClass(): void { $client = static::createClient(); @@ -83,12 +60,11 @@ public function testDebugBarContentNotResourceClass(): void $this->assertSame('Not an API Platform resource', $block->filterXPath('//div[@class="sf-toolbar-info-piece"][./b[contains(., "Resource Class")]]/span')->html()); } - /** - * @group legacy - */ + #[\PHPUnit\Framework\Attributes\Group('legacy')] public function testDebugBarContent(): void { $client = static::createClient(); + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class]); $client->enableProfiler(); $client->request('GET', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); $this->assertSame(200, $client->getResponse()->getStatusCode()); @@ -101,14 +77,9 @@ public function testDebugBarContent(): void // Check extra info content $this->assertStringContainsString('sf-toolbar-status-default', $block->attr('class'), 'The toolbar block should have the default color.'); - $this->assertSame('mongodb' === $this->env ? DocumentDummy::class : Dummy::class, $block->filterXPath('//div[@class="sf-toolbar-info-piece"][./b[contains(., "Resource Class")]]/span')->html()); + $this->assertSame($this->isMongoDB() ? DocumentDummy::class : Dummy::class, $block->filterXPath('//div[@class="sf-toolbar-info-piece"][./b[contains(., "Resource Class")]]/span')->html()); } - /** - * TODO: remove openapiContext to get rid of the legacy. - * - * @group legacy - */ public function testProfilerGeneralLayoutNotResourceClass(): void { $client = static::createClient(); @@ -133,6 +104,7 @@ public function testProfilerGeneralLayoutNotResourceClass(): void public function testProfilerGeneralLayout(): void { $client = static::createClient(); + $this->recreateSchema([Dummy::class, RelatedOwnedDummy::class, RelatedDummy::class]); $client->enableProfiler(); $client->request('GET', '/dummies', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); $this->assertSame(200, $client->getResponse()->getStatusCode()); @@ -145,7 +117,7 @@ public function testProfilerGeneralLayout(): void $metrics = $crawler->filter('.metrics'); $this->assertCount(1, $metrics->filter('.metric'), 'The should be one metric displayed (resource class).'); - $this->assertSame('mongodb' === $this->env ? DocumentDummy::class : Dummy::class, $metrics->filter('span.value')->html()); + $this->assertSame($this->isMongoDB() ? DocumentDummy::class : Dummy::class, $metrics->filter('span.value')->html()); $this->assertCount(4, $crawler->filter('.sf-tabs .tab-content'), 'Tabs must be presents on the panel.'); diff --git a/tests/Symfony/EventListener/AddFormatListenerTest.php b/tests/Symfony/EventListener/AddFormatListenerTest.php deleted file mode 100644 index 2b8a431d324..00000000000 --- a/tests/Symfony/EventListener/AddFormatListenerTest.php +++ /dev/null @@ -1,369 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Symfony\EventListener\AddFormatListener; -use Negotiation\Negotiator; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * @author Kévin Dunglas - * - * @group legacy - */ -class AddFormatListenerTest extends TestCase -{ - use ExpectDeprecationTrait; - use ProphecyTrait; - - public function testNoResourceClass(): void - { - $request = new Request(); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $listener = new AddFormatListener(new Negotiator()); - $listener->onKernelRequest($event); - - $this->assertNull($request->getRequestFormat(null)); - } - - public function testSupportedRequestFormat(): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: ['xml' => ['text/xml']]), - ]), - ])); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->setRequestFormat('xml'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('xml', $request->getRequestFormat()); - $this->assertSame('text/xml', $request->getMimeType($request->getRequestFormat())); - } - - public function testRespondFlag(): void - { - $request = new Request([], [], ['_api_respond' => true]); - $request->setRequestFormat('xml'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $listener = new AddFormatListener(new Negotiator(), null, ['xml' => ['text/xml']]); - $listener->onKernelRequest($event); - - $this->assertSame('xml', $request->getRequestFormat()); - $this->assertSame('text/xml', $request->getMimeType($request->getRequestFormat())); - } - - public function testUnsupportedRequestFormat(): void - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "text/xml" is not supported. Supported MIME types are "application/json".'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->setRequestFormat('xml'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: ['json' => ['application/json']]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('json', $request->getRequestFormat()); - } - - public function testSupportedAcceptHeader(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml, application/json;q=0.9, */*;q=0.8'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'binary' => ['application/octet-stream'], - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('json', $request->getRequestFormat()); - } - - public function testSupportedAcceptHeaderSymfonyBuiltInFormat(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'application/json'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'jsonld' => ['application/ld+json', 'application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('jsonld', $request->getRequestFormat()); - } - - public function testAcceptAllHeader(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'binary' => ['application/octet-stream'], - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('binary', $request->getRequestFormat()); - $this->assertSame('application/octet-stream', $request->getMimeType($request->getRequestFormat())); - } - - public function testUnsupportedAcceptHeader(): void - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "text/html, application/xhtml+xml, application/xml;q=0.9" is not supported. Supported MIME types are "application/octet-stream", "application/json".'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'text/html, application/xhtml+xml, application/xml;q=0.9'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'binary' => ['application/octet-stream'], - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testUnsupportedAcceptHeaderSymfonyBuiltInFormat(): void - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "text/xml" is not supported. Supported MIME types are "application/json".'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'text/xml'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testInvalidAcceptHeader(): void - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "invalid" is not supported. Supported MIME types are "application/json".'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'invalid'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testZeroAcceptHeader(): void - { - $this->expectException(NotAcceptableHttpException::class); - $this->expectExceptionMessage('Requested format "0" is not supported. Supported MIME types are "application/octet-stream", "application/json"'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', '0'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'binary' => ['application/octet-stream'], - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testAcceptHeaderTakePrecedenceOverRequestFormat(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->headers->set('Accept', 'application/json'); - $request->setRequestFormat('xml'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'xml' => ['application/xml'], - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('json', $request->getRequestFormat()); - } - - public function testInvalidRouteFormat(): void - { - $this->expectException(NotFoundHttpException::class); - $this->expectExceptionMessage('Format "invalid" is not supported'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_format' => 'invalid']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'json' => ['application/json'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - } - - public function testResourceClassSupportedRequestFormat(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->setRequestFormat('csv'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(outputFormats: [ - 'csv' => ['text/csv'], - ]), - ]), - ])); - - $listener = new AddFormatListener(new Negotiator(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($event); - - $this->assertSame('csv', $request->getRequestFormat()); - $this->assertSame('text/csv', $request->getMimeType($request->getRequestFormat())); - } -} diff --git a/tests/Symfony/EventListener/AddHeadersListenerTest.php b/tests/Symfony/EventListener/AddHeadersListenerTest.php deleted file mode 100644 index eda1f6f0604..00000000000 --- a/tests/Symfony/EventListener/AddHeadersListenerTest.php +++ /dev/null @@ -1,273 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Symfony\EventListener\AddHeadersListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @author Kévin Dunglas - */ -class AddHeadersListenerTest extends TestCase -{ - use ProphecyTrait; - - public function testDoNotSetHeaderWhenMethodNotCacheable(): void - { - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $request->setMethod('PUT'); - $response = new Response(); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddHeadersListener(true); - $listener->onKernelResponse($event); - - $this->assertNull($response->getEtag()); - } - - public function testDoNotSetHeaderOnUnsuccessfulResponse(): void - { - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $response = new Response('{}', Response::HTTP_BAD_REQUEST); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddHeadersListener(true); - $listener->onKernelResponse($event); - - $this->assertNull($response->getEtag()); - } - - public function testDoNotSetHeaderWhenNotAnApiOperation(): void - { - $response = new Response(); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request(), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddHeadersListener(true); - $listener->onKernelResponse($event); - - $this->assertNull($response->getEtag()); - } - - public function testDoNotSetHeaderWhenNoContent(): void - { - $response = new Response(); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - $listener = new AddHeadersListener(true); - $listener->onKernelResponse($event); - - $this->assertNull($response->getEtag()); - } - - public function testAddHeaders(): void - { - $response = new Response('some content', 200, ['Vary' => ['Accept', 'Cookie']]); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => new Get(name: 'get')])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, ['Accept', 'Accept-Encoding'], true, $factory->reveal(), 15, 30); - $listener->onKernelResponse($event); - - $this->assertSame('"9893532233caff98cd083a116b013c0b"', $response->getEtag()); - $this->assertSame('max-age=100, public, s-maxage=200, stale-if-error=30, stale-while-revalidate=15', $response->headers->get('Cache-Control')); - $this->assertEquals(['Accept', 'Cookie', 'Accept-Encoding'], $response->getVary()); - } - - public function testDoNotSetHeaderWhenAlreadySet(): void - { - $response = new Response('some content', 200, ['Vary' => ['Accept', 'Cookie']]); - $response->setEtag('etag'); - $response->setMaxAge(300); - // This also calls setPublic - $response->setSharedMaxAge(400); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => new Get(name: 'get')])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, ['Accept', 'Accept-Encoding'], true, $factory->reveal(), 15, 30); - $listener->onKernelResponse($event); - - $this->assertSame('"etag"', $response->getEtag()); - $this->assertSame('max-age=300, public, s-maxage=400, stale-if-error=30, stale-while-revalidate=15', $response->headers->get('Cache-Control')); - $this->assertEquals(['Accept', 'Cookie', 'Accept-Encoding'], $response->getVary()); - } - - public function testSetHeadersFromResourceMetadata(): void - { - $response = new Response('some content', 200, ['Vary' => ['Accept', 'Cookie']]); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $operation = new Get(name: 'get', cacheHeaders: ['max_age' => 123, 'shared_max_age' => 456, 'stale_while_revalidate' => 928, 'stale_if_error' => 70, 'vary' => ['Vary-1', 'Vary-2']]); - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => $operation])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, ['Accept', 'Accept-Encoding'], true, $factory->reveal(), 15, 30); - $listener->onKernelResponse($event); - - $this->assertSame('max-age=123, public, s-maxage=456, stale-if-error=70, stale-while-revalidate=928', $response->headers->get('Cache-Control')); - $this->assertEquals(['Accept', 'Cookie', 'Vary-1', 'Vary-2'], $response->getVary()); - } - - public function testSetHeadersFromResourceMetadataMarkedAsPrivate(): void - { - $response = new Response('some content', 200); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $operation = new Get(name: 'get', cacheHeaders: [ - 'max_age' => 123, - 'public' => false, - 'shared_max_age' => 456, - ]); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => $operation])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, [], true, $factory->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('max-age=123, private', $response->headers->get('Cache-Control')); - - // resource's cache marked as private must not contain s-maxage - $this->assertStringNotContainsString('s-maxage', $response->headers->get('Cache-Control')); - } - - public function testSetHeadersFromResourceMetadataMarkedAsPublic(): void - { - $response = new Response('some content', 200); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $operation = new Get(name: 'get', cacheHeaders: [ - 'max_age' => 123, - 'public' => true, - 'shared_max_age' => 456, - ]); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => $operation])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, [], true, $factory->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('max-age=123, public, s-maxage=456', $response->headers->get('Cache-Control')); - } - - public function testSetHeadersFromResourceMetadataWithNoPrivacy(): void - { - $response = new Response('some content', 200); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $operation = new Get(name: 'get', cacheHeaders: [ - 'max_age' => 123, - 'shared_max_age' => 456, - ]); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => $operation])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, [], true, $factory->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('max-age=123, public, s-maxage=456', $response->headers->get('Cache-Control')); - } - - public function testSetHeadersFromResourceMetadataWithNoPrivacyDefaultsPrivate(): void - { - $response = new Response('some content', 200); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $operation = new Get(name: 'get', cacheHeaders: [ - 'max_age' => 123, - 'shared_max_age' => 456, - ]); - - $factory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $factory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [new ApiResource(operations: ['get' => $operation])]))->shouldBeCalled(); - - $listener = new AddHeadersListener(true, 100, 200, ['Accept', 'Accept-Encoding'], false, $factory->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('max-age=123, private', $response->headers->get('Cache-Control')); - - // resource's cache marked as private must not contain s-maxage - $this->assertStringNotContainsString('s-maxage', $response->headers->get('Cache-Control')); - } -} diff --git a/tests/Symfony/EventListener/AddLinkHeaderListenerTest.php b/tests/Symfony/EventListener/AddLinkHeaderListenerTest.php deleted file mode 100644 index 047f389ec87..00000000000 --- a/tests/Symfony/EventListener/AddLinkHeaderListenerTest.php +++ /dev/null @@ -1,136 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Symfony\EventListener\AddLinkHeaderListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Mercure\Discovery; -use Symfony\Component\Mercure\Hub; -use Symfony\Component\Mercure\HubRegistry; -use Symfony\Component\Mercure\Jwt\StaticTokenProvider; -use Symfony\Component\WebLink\GenericLinkProvider; -use Symfony\Component\WebLink\HttpHeaderSerializer; -use Symfony\Component\WebLink\Link; - -/** - * @author Kévin Dunglas - */ -class AddLinkHeaderListenerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider addProvider - */ - public function testAddLinkHeader(string $expected, Request $request): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(mercure: [ - 'hub' => 'managed', - ]), - ]), - ])); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new Response() - ); - - $defaultHub = new Hub('https://internal/.well-known/mercure', new StaticTokenProvider('xxx'), null, 'https://external/.well-known/mercure'); - $managedHub = new Hub('https://managed.mercure.rocks/.well-known/mercure', new StaticTokenProvider('xxx'), null, 'https://managed.mercure.rocks/.well-known/mercure'); - - $mercure = new HubRegistry($defaultHub, ['default' => $defaultHub, 'managed' => $managedHub]); - $discovery = new Discovery($mercure); - - $listener = new AddLinkHeaderListener($discovery, $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelResponse($event); - $this->assertSame($expected, (new HttpHeaderSerializer())->serialize($request->attributes->get('_links')->getLinks())); - } - - public static function addProvider(): array - { - return [ - ['; rel="mercure"', new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get'])], - ['; rel="http://www.w3.org/ns/hydra/core#apiDocumentation",; rel="mercure"', new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_links' => new GenericLinkProvider([new Link('http://www.w3.org/ns/hydra/core#apiDocumentation', 'http://example.com/docs')])])], - ]; - } - - /** - * @dataProvider doNotAddProvider - */ - public function testDoNotAddHeader(Request $request): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [new Get()]), - ])); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new Response() - ); - - $defaultHub = new Hub('https://internal/.well-known/mercure', new StaticTokenProvider('xxx'), null, 'https://external/.well-known/mercure'); - $registry = new HubRegistry($defaultHub, ['default' => $defaultHub]); - $listener = new AddLinkHeaderListener(new Discovery($registry), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertNull($request->attributes->get('_links')); - } - - public static function doNotAddProvider(): array - { - return [ - [new Request()], - [new Request([], [], ['_api_resource_class' => Dummy::class])], - ]; - } - - public function testSkipWhenPreflightRequest(): void - { - $request = new Request(); - $request->setMethod('OPTIONS'); - $request->headers->set('Access-Control-Request-Method', 'POST'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new Response() - ); - - $defaultHub = new Hub('https://internal/.well-known/mercure', new StaticTokenProvider('xxx'), null, 'https://external/.well-known/mercure'); - $registry = new HubRegistry($defaultHub, ['default' => $defaultHub]); - $listener = new AddLinkHeaderListener(new Discovery($registry)); - $listener->onKernelResponse($event); - - $this->assertFalse($request->attributes->has('_links')); - } -} diff --git a/tests/Symfony/EventListener/AddTagsListenerTest.php b/tests/Symfony/EventListener/AddTagsListenerTest.php deleted file mode 100644 index e878f90ae4d..00000000000 --- a/tests/Symfony/EventListener/AddTagsListenerTest.php +++ /dev/null @@ -1,286 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Symfony\EventListener\AddTagsListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @author Kévin Dunglas - */ -class AddTagsListenerTest extends TestCase -{ - use ProphecyTrait; - - public const DEFAULT_CACHE_TAG = 'Cache-Tags'; - - public function testDoNotSetHeaderWhenMethodNotCacheable(): void - { - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - - $request = new Request([], [], ['_resources' => ['/foo', '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $request->setMethod('PUT'); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertFalse($response->headers->has(self::DEFAULT_CACHE_TAG)); - } - - public function testDoNotSetHeaderWhenResponseNotCacheable(): void - { - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - - $request = new Request([], [], ['_resources' => ['/foo', '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $response = new Response(); - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertFalse($response->headers->has(self::DEFAULT_CACHE_TAG)); - } - - public function testDoNotSetHeaderWhenNotAnApiOperation(): void - { - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo', '/bar']]), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertFalse($response->headers->has(self::DEFAULT_CACHE_TAG)); - } - - public function testDoNotSetHeaderWhenEmptyTagList(): void - { - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => [], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertFalse($response->headers->has(self::DEFAULT_CACHE_TAG)); - } - - public function testAddTags(): void - { - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo', '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('/foo,/bar', $response->headers->get('Cache-Tags')); - } - - public function testAddCollectionIri(): void - { - $operation = (new GetCollection()); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation, Argument::type('array'))->willReturn('/dummies')->shouldBeCalled(); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo', '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_operation' => $operation]), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('/foo,/bar,/dummies', $response->headers->get(self::DEFAULT_CACHE_TAG)); - } - - public function testAddCollectionIriWhenCollectionIsEmpty(): void - { - $operation = (new GetCollection()); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation, Argument::type('array'))->willReturn('/dummies')->shouldBeCalled(); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => [], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_operation' => $operation]), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $listener = new AddTagsListener($iriConverterProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('/dummies', $response->headers->get(self::DEFAULT_CACHE_TAG)); - } - - public function testAddTagsWithXKey(): void - { - $operation = (new GetCollection(name: 'get')); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation, Argument::type('array'))->willReturn('/dummies')->shouldBeCalled(); - - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $dummyMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations(['get' => $operation]))]); - $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn($dummyMetadata); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->getResponseHeaders(['/foo' => '/foo', '/bar' => '/bar', '/dummies' => '/dummies'])->willReturn(['xkey' => '/foo /bar /dummies']); - - $listener = new AddTagsListener($iriConverterProphecy->reveal(), $resourceMetadataCollectionFactoryProphecy->reveal(), $purgerProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertSame('/foo /bar /dummies', $response->headers->get('xkey')); - } - - public function testAddTagsWithoutHeader(): void - { - $operation = (new GetCollection(name: 'get')); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation, Argument::type('array'))->willReturn('/dummies')->shouldBeCalled(); - - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $dummyMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations(['get' => $operation]))]); - $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn($dummyMetadata); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->getResponseHeaders(['/foo' => '/foo', '/bar' => '/bar', '/dummies' => '/dummies'])->willReturn([]); - - $listener = new AddTagsListener($iriConverterProphecy->reveal(), $resourceMetadataCollectionFactoryProphecy->reveal(), $purgerProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertNull($response->headers->get('xkey')); - } - - public function testDummyHeaderTag(): void - { - $operation = (new GetCollection(name: 'get')); - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation, Argument::type('array'))->willReturn('/dummies')->shouldBeCalled(); - - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $dummyMetadata = new ResourceMetadataCollection(Dummy::class, [(new ApiResource())->withOperations(new Operations(['get' => $operation]))]); - $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn($dummyMetadata); - - $response = new Response(); - $response->setPublic(); - $response->setEtag('foo'); - - $event = new ResponseEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_resources' => ['/foo' => '/foo', '/bar' => '/bar'], '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->getResponseHeaders(['/foo' => '/foo', '/bar' => '/bar', '/dummies' => '/dummies'])->willReturn(['Dummy-Header' => '/foo,/bar,/dummies']); - - $listener = new AddTagsListener($iriConverterProphecy->reveal(), $resourceMetadataCollectionFactoryProphecy->reveal(), $purgerProphecy->reveal()); - $listener->onKernelResponse($event); - - $this->assertNull($response->headers->get(self::DEFAULT_CACHE_TAG)); - $this->assertNull($response->headers->get('xkey')); - $this->assertSame('/foo,/bar,/dummies', $response->headers->get('Dummy-Header')); - } -} diff --git a/tests/Symfony/EventListener/DenyAccessListenerTest.php b/tests/Symfony/EventListener/DenyAccessListenerTest.php deleted file mode 100644 index 62a9f3f2ce8..00000000000 --- a/tests/Symfony/EventListener/DenyAccessListenerTest.php +++ /dev/null @@ -1,166 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Symfony\EventListener\DenyAccessListener; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; - -/** - * @author Kévin Dunglas - */ -class DenyAccessListenerTest extends TestCase -{ - use ProphecyTrait; - - public function testNoResourceClass(): void - { - $request = new Request(); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldNotBeCalled(); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); - - $listener = $this->getListener($resourceMetadataFactory); - $listener->onSecurity($event); - } - - public function testNoIsGrantedAttribute(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(), - ]), - ])); - - $listener = $this->getListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onSecurity($event); - } - - public function testIsGranted(): void - { - $data = new \stdClass(); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', 'data' => $data]); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(security: 'is_granted("ROLE_ADMIN")'), - ]), - ])); - - $resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - $resourceAccessCheckerProphecy->isGranted('Foo', 'is_granted("ROLE_ADMIN")', Argument::type('array'))->willReturn(true)->shouldBeCalled(); - - $listener = $this->getListener($resourceMetadataFactoryProphecy->reveal(), $resourceAccessCheckerProphecy->reveal()); - $listener->onSecurity($event); - } - - public function testIsNotGranted(): void - { - $this->expectException(AccessDeniedException::class); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(security: 'is_granted("ROLE_ADMIN")'), - ]), - ])); - - $resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - $resourceAccessCheckerProphecy->isGranted('Foo', 'is_granted("ROLE_ADMIN")', Argument::type('array'))->willReturn(false)->shouldBeCalled(); - - $listener = $this->getListener($resourceMetadataFactoryProphecy->reveal(), $resourceAccessCheckerProphecy->reveal()); - $listener->onSecurity($event); - } - - public function testSecurityMessage(): void - { - $this->expectException(AccessDeniedException::class); - $this->expectExceptionMessage('You are not admin.'); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'get' => new Get(security: 'is_granted("ROLE_ADMIN")', securityMessage: 'You are not admin.'), - ]), - ])); - - $resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - $resourceAccessCheckerProphecy->isGranted('Foo', 'is_granted("ROLE_ADMIN")', Argument::type('array'))->willReturn(false)->shouldBeCalled(); - - $listener = $this->getListener($resourceMetadataFactoryProphecy->reveal(), $resourceAccessCheckerProphecy->reveal()); - $listener->onSecurity($event); - } - - public function testSecurityComponentNotAvailable(): void - { - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - $event = $eventProphecy->reveal(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldNotBeCalled(); - - $listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onSecurity($event); - } - - private function getListener(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ?ResourceAccessCheckerInterface $resourceAccessChecker = null): DenyAccessListener - { - if (null === $resourceAccessChecker) { - $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class)->reveal(); - } - - return new DenyAccessListener($resourceMetadataCollectionFactory, $resourceAccessChecker); - } -} diff --git a/tests/Symfony/EventListener/DeserializeListenerTest.php b/tests/Symfony/EventListener/DeserializeListenerTest.php deleted file mode 100644 index 2b0845e22d3..00000000000 --- a/tests/Symfony/EventListener/DeserializeListenerTest.php +++ /dev/null @@ -1,427 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Symfony\EventListener\DeserializeListener; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; -use Symfony\Component\Serializer\Exception\PartialDenormalizationException; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Translation\IdentityTranslator; - -/** - * @author Kévin Dunglas - * - * @group legacy - */ -class DeserializeListenerTest extends TestCase -{ - use ProphecyTrait; - - private const FORMATS = ['json' => ['application/json']]; - - public function testDoNotCallWhenRequestMethodIsSafe(): void - { - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['data' => new \stdClass()]); - $request->setMethod('GET'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->shouldNotBeCalled(); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create()->shouldNotBeCalled(); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactory->reveal()); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testDoNotCallWhenRequestNotManaged(): void - { - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['data' => new \stdClass()], [], [], [], '{}'); - $request->setMethod('POST'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->shouldNotBeCalled(); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create()->shouldNotBeCalled(); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactory->reveal()); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testDoNotDeserializeWhenReceiveFlagIsFalse(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create()->shouldNotBeCalled(); - - $request = new Request([], [], ['data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_operation_name' => 'post', '_api_receive' => false]); - $request->setMethod('POST'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testDoNotDeserializeWhenDisabledInOperationAttribute(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(deserialize: false), - ]), - ])); - - $request = new Request([], [], ['data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_operation_name' => 'post']); - $request->setMethod('POST'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $eventProphecy->getRequest()->willReturn($request); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - /** - * @dataProvider methodProvider - */ - public function testDeserialize(string $method, bool $populateObject): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'put' => new Put(inputFormats: self::FORMATS), - 'post' => new Post(inputFormats: self::FORMATS), - ]), - ])); - - $this->doTestDeserialize($method, $populateObject, $resourceMetadataFactoryProphecy->reveal()); - } - - private function doTestDeserialize(string $method, bool $populateObject, $resourceMetadataFactory): void - { - $result = $populateObject ? new \stdClass() : null; - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['data' => $result, '_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod($method); - $request->headers->set('Content-Type', 'application/json'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : []; - $context['input'] = ['class' => 'Foo']; - $context['output'] = ['class' => 'Foo']; - $context['resource_class'] = 'Foo'; - $serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo'])->shouldBeCalled(); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactory); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - /** - * @dataProvider methodProvider - */ - public function testDeserializeResourceClassSupportedFormat(string $method, bool $populateObject): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(inputFormats: self::FORMATS), - ]), - ])); - - $this->doTestDeserializeResourceClassSupportedFormat($method, $populateObject, $resourceMetadataFactoryProphecy->reveal()); - } - - private function doTestDeserializeResourceClassSupportedFormat(string $method, bool $populateObject, $resourceMetadataFactory): void - { - $result = $populateObject ? new \stdClass() : null; - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['data' => $result, '_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod($method); - $request->headers->set('Content-Type', 'application/json'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $context = $populateObject ? [AbstractNormalizer::OBJECT_TO_POPULATE => $populateObject] : []; - $context['input'] = ['class' => 'Foo']; - $context['output'] = ['class' => 'Foo']; - $context['resource_class'] = 'Foo'; - $serializerProphecy->deserialize('{}', 'Foo', 'json', $context)->willReturn($result)->shouldBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo'])->shouldBeCalled(); - - $listener = new DeserializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactory); - - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public static function methodProvider(): iterable - { - yield ['POST', false]; - yield ['PUT', true]; - } - - public function testContentNegotiation(): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(inputFormats: ['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']]), - ]), - ])); - - $this->doTestContentNegotiation($resourceMetadataFactoryProphecy->reveal()); - } - - private function doTestContentNegotiation($resourceMetadataFactory): void - { - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod('POST'); - $request->headers->set('Content-Type', 'text/xml'); - $request->setFormat('xml', 'text/xml'); // Workaround to avoid weird behaviors - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $context = ['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo']; - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize('{}', 'Foo', 'xml', $context)->willReturn(new \stdClass())->shouldBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn($context)->shouldBeCalled(); - - $listener = new DeserializeListener( - $serializerProphecy->reveal(), - $serializerContextBuilderProphecy->reveal(), - $resourceMetadataFactory - ); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testNotSupportedContentType(): void - { - $this->expectException(UnsupportedMediaTypeHttpException::class); - $this->expectExceptionMessage('The content-type "application/rdf+xml" is not supported. Supported MIME types are "application/ld+json", "text/xml".'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod('POST'); - $request->headers->set('Content-Type', 'application/rdf+xml'); - $request->setRequestFormat('xml'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo']]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(inputFormats: ['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']]), - ]), - ])); - - $listener = new DeserializeListener( - $serializerProphecy->reveal(), - $serializerContextBuilderProphecy->reveal(), - $resourceMetadataFactoryProphecy->reveal() - ); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testNoContentType(): void - { - $this->expectException(UnsupportedMediaTypeHttpException::class); - $this->expectExceptionMessage('The "Content-Type" header must exist.'); - - $eventProphecy = $this->prophesize(RequestEvent::class); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod('POST'); - $request->setRequestFormat('unknown'); - $eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo']]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(formats: ['jsonld' => ['application/ld+json'], 'xml' => ['text/xml']]), - ]), - ])); - - $listener = new DeserializeListener( - $serializerProphecy->reveal(), - $serializerContextBuilderProphecy->reveal(), - $resourceMetadataFactoryProphecy->reveal() - ); - $listener->onKernelRequest($eventProphecy->reveal()); - } - - public function testTurnPartialDenormalizationExceptionIntoValidationException(): void - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create('Foo')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('Foo', [ - new ApiResource(operations: [ - 'post' => new Post(inputFormats: self::FORMATS), - ]), - ])); - $notNormalizableValueException = NotNormalizableValueException::createForUnexpectedDataType('Message for user (hint)', [], ['bool', 'string'], 'foo', true, 0); - $partialDenormalizationException = new PartialDenormalizationException('', [$notNormalizableValueException]); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->willThrow($partialDenormalizationException); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), false, Argument::type('array'))->willReturn(['input' => ['class' => 'Foo'], 'output' => ['class' => 'Foo'], 'resource_class' => 'Foo']); - - $eventProphecy = $this->prophesize(RequestEvent::class); - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'post'], [], [], [], '{}'); - $request->setMethod('POST'); - $request->headers->set('Content-Type', 'application/json'); - $eventProphecy->getRequest()->willReturn($request); - - $listener = new DeserializeListener( - $serializerProphecy->reveal(), - $serializerContextBuilderProphecy->reveal(), - $resourceMetadataFactoryProphecy->reveal(), - new IdentityTranslator(), - ); - - try { - $listener->onKernelRequest($eventProphecy->reveal()); - $this->fail('Test failed, a ValidationException should have been thrown'); - } catch (ValidationException $e) { - $this->assertCount(1, $e->getConstraintViolationList()); - $list = $e->getConstraintViolationList(); - $violation = $list->get(0); - $this->assertSame($violation->getMessage(), 'This value should be of type bool|string.'); - $this->assertSame($violation->getMessageTemplate(), 'This value should be of type {{ type }}.'); - $this->assertSame([ - 'hint' => 'Message for user (hint)', - ], $violation->getParameters()); - $this->assertNull($violation->getRoot()); - $this->assertSame($violation->getPropertyPath(), 'foo'); - $this->assertNull($violation->getInvalidValue()); - $this->assertNull($violation->getPlural()); - $this->assertSame($violation->getCode(), 'ba785a8c-82cb-4283-967c-3cf342181b40'); - } - } - - public function testRequestWithEmptyContentType(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::cetera())->willReturn([]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Argument::cetera())->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'post' => new Post(inputFormats: self::FORMATS), - ]), - ]))->shouldBeCalled(); - - $listener = new DeserializeListener( - $serializerProphecy->reveal(), - $serializerContextBuilderProphecy->reveal(), - $resourceMetadataFactoryProphecy->reveal() - ); - - // in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request - // without a content-type and content-length header will result in the - // variables set to an empty string, not null - - $request = new Request( - server: [ - 'REQUEST_METHOD' => 'POST', - 'REQUEST_URI' => '/', - 'CONTENT_TYPE' => '', - 'CONTENT_LENGTH' => '', - ], - attributes: [ - '_api_resource_class' => Dummy::class, - '_api_operation_name' => 'post', - '_api_receive' => true, - ], - content: '' - ); - - $event = new RequestEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - ); - - $this->expectException(UnsupportedMediaTypeHttpException::class); - $this->expectExceptionMessage('The "Content-Type" header must exist.'); - - $listener->onKernelRequest($event); - } -} diff --git a/tests/Symfony/EventListener/EventPrioritiesTest.php b/tests/Symfony/EventListener/EventPrioritiesTest.php deleted file mode 100644 index 9338ef6a347..00000000000 --- a/tests/Symfony/EventListener/EventPrioritiesTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Symfony\EventListener\EventPriorities; -use PHPUnit\Framework\TestCase; - -/** - * @author Kévin Dunglas - */ -class EventPrioritiesTest extends TestCase -{ - public function testConstants(): void - { - $this->assertSame(5, EventPriorities::PRE_READ); - $this->assertSame(3, EventPriorities::POST_READ); - $this->assertSame(3, EventPriorities::PRE_DESERIALIZE); - $this->assertSame(1, EventPriorities::POST_DESERIALIZE); - $this->assertSame(65, EventPriorities::PRE_VALIDATE); - $this->assertSame(63, EventPriorities::POST_VALIDATE); - $this->assertSame(33, EventPriorities::PRE_WRITE); - $this->assertSame(31, EventPriorities::POST_WRITE); - $this->assertSame(17, EventPriorities::PRE_SERIALIZE); - $this->assertSame(15, EventPriorities::POST_SERIALIZE); - $this->assertSame(9, EventPriorities::PRE_RESPOND); - $this->assertSame(0, EventPriorities::POST_RESPOND); - } -} diff --git a/tests/Symfony/EventListener/ExceptionListenerTest.php b/tests/Symfony/EventListener/ExceptionListenerTest.php deleted file mode 100644 index 8d259a821f9..00000000000 --- a/tests/Symfony/EventListener/ExceptionListenerTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Symfony\EventListener\ExceptionListener; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\HttpKernel\EventListener\ErrorListener; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @author Kévin Dunglas - */ -class ExceptionListenerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider getRequest - */ - public function testOnKernelException(Request $request): void - { - $kernel = $this->prophesize(HttpKernelInterface::class); - - $event = new ExceptionEvent($kernel->reveal(), $request, HttpKernelInterface::MAIN_REQUEST, new \Exception()); - - $errorListener = $this->prophesize(ErrorListener::class); - $errorListener->onKernelException($event)->shouldBeCalled(); - $listener = new ExceptionListener($errorListener->reveal()); - $listener->onKernelException($event); - } - - public static function getRequest(): array - { - return [ - [new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get'])], - [new Request([], [], ['_api_respond' => true])], - ]; - } - - public function testDoNothingWhenNotAnApiCall(): void - { - $errorListener = $this->prophesize(ErrorListener::class); - $listener = new ExceptionListener($errorListener->reveal()); - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, new \Exception()); - $listener->onKernelException($event); - - $this->assertNull($event->getResponse()); - } - - public function testDoNothingWhenHtmlRequested(): void - { - $request = new Request([], [], ['_api_respond' => true]); - $request->setRequestFormat('html'); - - $errorListener = $this->prophesize(ErrorListener::class); - $listener = new ExceptionListener($errorListener->reveal()); - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), $request, HttpKernelInterface::MAIN_REQUEST, new \Exception()); - $listener->onKernelException($event); - - $this->assertNull($event->getResponse()); - } -} diff --git a/tests/Symfony/EventListener/ReadListenerTest.php b/tests/Symfony/EventListener/ReadListenerTest.php deleted file mode 100644 index fb78c7e3339..00000000000 --- a/tests/Symfony/EventListener/ReadListenerTest.php +++ /dev/null @@ -1,123 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Api\UriVariablesConverterInterface; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Link; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\State\CallableProvider; -use ApiPlatform\State\ProviderInterface; -use ApiPlatform\Symfony\EventListener\ReadListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @group legacy - */ -class ReadListenerTest extends TestCase -{ - use ProphecyTrait; - - public function testNotAnApiPlatformRequest(): void - { - $event = $this->prophesize(RequestEvent::class); - $event->getRequest()->willReturn(new Request())->shouldBeCalled(); - - $provider = $this->prophesize(ProviderInterface::class); - $provider->provide()->shouldNotBeCalled(); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); - $uriVariablesConverter = $this->prophesize(UriVariablesConverterInterface::class); - $listener = new ReadListener($provider->reveal(), $resourceMetadataCollectionFactory->reveal(), $serializerContextBuilder->reveal(), $uriVariablesConverter->reveal()); - $listener->onKernelRequest($event->reveal()); - } - - public function testDoNotReadWhenReceiveFlagIsFalse(): void - { - $request = new Request([], [], ['id' => 1, 'data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_operation_name' => 'put', '_api_receive' => false]); - $request->setMethod('PUT'); - - $event = $this->prophesize(RequestEvent::class); - $event->getRequest()->willReturn($request); - - $provider = $this->prophesize(ProviderInterface::class); - $provider->provide()->shouldNotBeCalled(); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - (new ApiResource())->withOperations(new Operations([ - 'put' => new Put(), - ])), - ])); - $serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); - $uriVariablesConverter = $this->prophesize(UriVariablesConverterInterface::class); - $listener = new ReadListener($provider->reveal(), $resourceMetadataCollectionFactory->reveal(), $serializerContextBuilder->reveal(), $uriVariablesConverter->reveal()); - $listener->onKernelRequest($event->reveal()); - } - - public function testDoNotReadWhenReadIsFalse(): void - { - $request = new Request([], [], ['id' => 1, 'data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_operation_name' => 'put']); - $request->setMethod('PUT'); - - $event = $this->prophesize(RequestEvent::class); - $event->getRequest()->willReturn($request); - - $provider = new CallableProvider(); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - (new ApiResource())->withOperations(new Operations([ - 'put' => (new Put())->withRead(false), - ])), - ])); - $serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); - $listener = new ReadListener($provider, $resourceMetadataCollectionFactory->reveal(), $serializerContextBuilder->reveal()); - $listener->onKernelRequest($event->reveal()); - } - - public function readWithIdentifiers(): void - { - $request = new Request([], [], ['id' => '1', 'data' => new Dummy(), '_api_resource_class' => Dummy::class, '_api_operation_name' => 'get']); - $request->setMethod('GET'); - - $event = $this->prophesize(RequestEvent::class); - $event->getRequest()->willReturn($request); - - $operation = (new Get())->withUriVariables(['id' => (new Link())->withFromClass(Dummy::class)->withIdentifiers(['id'])]); - $provider = $this->prophesize(ProviderInterface::class); - $provider->provide(Dummy::class, ['id' => 1], 'get', Argument::type('array'))->shouldNotBeCalled(); - $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - (new ApiResource())->withOperations(new Operations([ - 'get' => $operation, - ])), - ])); - $serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilder->createFromRequest($request, true, Argument::type('array'))->shouldBeCalled()->willReturn(['groups' => ['a']]); - $uriVariablesConverter = $this->prophesize(UriVariablesConverterInterface::class); - $uriVariablesConverter->convert(['id' => '1'], Dummy::class, ['operation' => $operation])->shouldBeCalled()->willReturn(['id' => 1]); - - $listener = new ReadListener($provider->reveal(), $resourceMetadataCollectionFactory->reveal(), $serializerContextBuilder->reveal(), $uriVariablesConverter->reveal()); - $listener->onKernelRequest($event->reveal()); - } -} diff --git a/tests/Symfony/EventListener/RespondListenerTest.php b/tests/Symfony/EventListener/RespondListenerTest.php deleted file mode 100644 index 517bea72585..00000000000 --- a/tests/Symfony/EventListener/RespondListenerTest.php +++ /dev/null @@ -1,277 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Symfony\EventListener\RespondListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ViewEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @author Kévin Dunglas - * - * @group legacy - */ -class RespondListenerTest extends TestCase -{ - use ProphecyTrait; - - public function testDoNotHandleResponse(): void - { - $listener = new RespondListener(); - $event = new ViewEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, null); - $listener->onKernelView($event); - - $this->assertNull($event->getResponse()); - } - - public function testDoNotHandleWhenRespondFlagIsFalse(): void - { - $listener = new RespondListener(); - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_respond' => false]), - HttpKernelInterface::MAIN_REQUEST, - 'foo' - ); - $listener->onKernelView($event); - - $this->assertNull($event->getResponse()); - } - - public function testCreate200Response(): void - { - $request = new Request([], [], ['_api_respond' => true]); - $request->setRequestFormat('xml'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'foo' - ); - - $listener = new RespondListener(); - $listener->onKernelView($event); - - $response = $event->getResponse(); - $this->assertSame('foo', $response->getContent()); - $this->assertSame(Response::HTTP_OK, $response->getStatusCode()); - $this->assertSame('text/xml; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('Accept', $response->headers->get('Vary')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - } - - public function testPost200WithoutLocation(): void - { - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'post', '_api_respond' => true, '_api_write_item_iri' => '/dummy_entities/1']); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'bar' - ); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'post' => new Post(status: Response::HTTP_OK), - ]), - ])); - - $listener = new RespondListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $response = $event->getResponse(); - $this->assertFalse($response->headers->has('Location')); - $this->assertSame(Response::HTTP_OK, $event->getResponse()->getStatusCode()); - } - - public function testPost301WithLocation(): void - { - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_respond' => true, '_api_write_item_iri' => '/dummy_entities/1']); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'bar' - ); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(status: Response::HTTP_MOVED_PERMANENTLY), - ]), - ])); - - $listener = new RespondListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $response = $event->getResponse(); - $this->assertTrue($response->headers->has('Location')); - $this->assertSame('/dummy_entities/1', $response->headers->get('Location')); - $this->assertSame(Response::HTTP_MOVED_PERMANENTLY, $event->getResponse()->getStatusCode()); - } - - public function testCreate201Response(): void - { - $request = new Request([], [], ['_api_respond' => true, '_api_write_item_iri' => '/dummy_entities/1']); - $request->setMethod('POST'); - $request->setRequestFormat('xml'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'foo' - ); - - $listener = new RespondListener(); - $listener->onKernelView($event); - - $response = $event->getResponse(); - $this->assertSame('foo', $response->getContent()); - $this->assertSame(Response::HTTP_CREATED, $response->getStatusCode()); - $this->assertSame('text/xml; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('Accept', $response->headers->get('Vary')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - $this->assertSame('/dummy_entities/1', $response->headers->get('Location')); - $this->assertSame('/dummy_entities/1', $response->headers->get('Content-Location')); - $this->assertTrue($response->headers->has('Location')); - } - - public function testCreate204Response(): void - { - $request = new Request([], [], ['_api_respond' => true]); - $request->setRequestFormat('xml'); - $request->setMethod('DELETE'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'foo' - ); - - $listener = new RespondListener(); - $listener->onKernelView($event); - - $response = $event->getResponse(); - $this->assertSame('foo', $response->getContent()); - $this->assertSame(Response::HTTP_NO_CONTENT, $response->getStatusCode()); - $this->assertSame('text/xml; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('Accept', $response->headers->get('Vary')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - } - - public function testSetSunsetHeader(): void - { - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_respond' => true]), - HttpKernelInterface::MAIN_REQUEST, - 'bar' - ); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(sunset: 'tomorrow'), - ]), - ])); - - $listener = new RespondListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $response = $event->getResponse(); - /** @var string $value */ - $value = $response->headers->get('Sunset'); - $this->assertEquals(new \DateTimeImmutable('tomorrow'), \DateTime::createFromFormat(\DATE_RFC1123, $value)); - } - - public function testSetCustomStatus(): void - { - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_respond' => true]), - HttpKernelInterface::MAIN_REQUEST, - 'bar' - ); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'get' => new Get(status: Response::HTTP_ACCEPTED), - ]), - ])); - - $listener = new RespondListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertSame(Response::HTTP_ACCEPTED, $event->getResponse()->getStatusCode()); - } - - public function testSetCustomStatusForPut(): void - { - $request = new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'put', '_api_respond' => true], [], [], ['REQUEST_METHOD' => 'PUT']); - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - 'bar' - ); - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'put' => new Put(status: Response::HTTP_ACCEPTED), - ]), - ])); - - $listener = new RespondListener($resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertSame(Response::HTTP_ACCEPTED, $event->getResponse()->getStatusCode()); - } - - public function testHandleResponse(): void - { - $listener = new RespondListener(); - - $response = new Response(); - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request([], [], ['_api_resource_class' => Dummy::class, '_api_operation_name' => 'get', '_api_respond' => true]), - HttpKernelInterface::MAIN_REQUEST, - $response - ); - $listener->onKernelView($event); - - $this->assertSame($response, $event->getResponse()); - } -} diff --git a/tests/Symfony/EventListener/SerializeListenerTest.php b/tests/Symfony/EventListener/SerializeListenerTest.php deleted file mode 100644 index f4f401f4f05..00000000000 --- a/tests/Symfony/EventListener/SerializeListenerTest.php +++ /dev/null @@ -1,240 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\ResourceList; -use ApiPlatform\State\SerializerContextBuilderInterface; -use ApiPlatform\Symfony\EventListener\SerializeListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\ViewEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Serializer\Encoder\EncoderInterface; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * @author Kévin Dunglas - * - * @group legacy - */ -class SerializeListenerTest extends TestCase -{ - use ProphecyTrait; - - public function testDoNotSerializeWhenControllerResultIsResponse(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::cetera()); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request(), - HttpKernelInterface::MAIN_REQUEST, - null - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertNull($event->getResponse()); - } - - public function testDoNotSerializeWhenRespondFlagIsFalse(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $dummy = new Dummy(); - - $request = new Request([], [], ['data' => $dummy, '_api_resource_class' => Dummy::class, '_api_operation_name' => 'post', '_api_respond' => false]); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $dummy - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertNull($event->getResponse()); - } - - public function testDoNotSerializeWhenDisabledInOperationAttribute(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ - new ApiResource(operations: [ - 'post' => new Post(serialize: false), - ]), - ])); - - $dummy = new Dummy(); - $request = new Request([], [], ['data' => $dummy, '_api_resource_class' => Dummy::class, '_api_operation_name' => 'post']); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $dummy - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertNull($event->getResponse()); - } - - public function testSerializeCollectionOperation(): void - { - $expectedContext = ['request_uri' => '', 'resource_class' => 'Foo', 'operation_name' => 'get']; - $serializerProphecy = $this->prophesize(SerializerInterface::class); - - $serializerProphecy - ->serialize( - Argument::any(), - 'xml', - Argument::allOf( - Argument::that(fn (array $context) => $context['resources'] instanceof ResourceList && $context['resources_to_push'] instanceof ResourceList), - Argument::withEntry('request_uri', ''), - Argument::withEntry('resource_class', 'Foo'), - Argument::withEntry('operation_name', 'get') - ) - ) - ->willReturn('bar') - ->shouldBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), true, Argument::type('array'))->willReturn($expectedContext)->shouldBeCalled(); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->setRequestFormat('xml'); - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new \stdClass() - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertSame('bar', $event->getControllerResult()); - } - - public function testSerializeCollectionOperationWithOutputClassDisabled(): void - { - $expectedContext = ['request_uri' => '', 'resource_class' => 'Foo', 'operation_name' => 'post', 'output' => ['class' => null]]; - $serializerProphecy = $this->prophesize(SerializerInterface::class); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), true, Argument::type('array'))->willReturn($expectedContext)->shouldBeCalled(); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_output_class' => false]); - $request->setRequestFormat('xml'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new \stdClass() - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertNull($event->getControllerResult()); - } - - public function testSerializeItemOperation(): void - { - $expectedContext = ['request_uri' => '', 'resource_class' => 'Foo', 'operation_name' => 'get']; - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy - ->serialize( - Argument::any(), - 'xml', - Argument::allOf( - Argument::that(fn (array $context) => $context['resources'] instanceof ResourceList && $context['resources_to_push'] instanceof ResourceList), - Argument::withEntry('request_uri', ''), - Argument::withEntry('resource_class', 'Foo'), - Argument::withEntry('operation_name', 'get') - ) - ) - ->willReturn('bar') - ->shouldBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $serializerContextBuilderProphecy->createFromRequest(Argument::type(Request::class), true, Argument::type('array'))->willReturn($expectedContext)->shouldBeCalled(); - - $request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_operation_name' => 'get']); - $request->setRequestFormat('xml'); - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - new \stdClass() - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertSame('bar', $event->getControllerResult()); - } - - public function testEncode(): void - { - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->willImplement(EncoderInterface::class); - $serializerProphecy->encode(Argument::any(), 'xml')->willReturn('bar')->shouldBeCalled(); - $serializerProphecy->serialize(Argument::cetera())->shouldNotBeCalled(); - - $serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $request = new Request([], [], ['_api_respond' => true]); - $request->setRequestFormat('xml'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - [] - ); - - $listener = new SerializeListener($serializerProphecy->reveal(), $serializerContextBuilderProphecy->reveal()); - $listener->onKernelView($event); - - $this->assertSame('bar', $event->getControllerResult()); - } -} diff --git a/tests/Symfony/EventListener/WriteListenerTest.php b/tests/Symfony/EventListener/WriteListenerTest.php deleted file mode 100644 index 440f4741616..00000000000 --- a/tests/Symfony/EventListener/WriteListenerTest.php +++ /dev/null @@ -1,309 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\EventListener; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Delete; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Link; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Patch; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\Symfony\EventListener\WriteListener; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OperationResource; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ViewEvent; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @group legacy - */ -class WriteListenerTest extends TestCase -{ - use ProphecyTrait; - - private ObjectProphecy $processorProphecy; - private ObjectProphecy $iriConverterProphecy; - private ObjectProphecy $resourceMetadataCollectionFactory; - private ObjectProphecy $resourceClassResolver; - - public static function noopProcessor($data) - { - return $data; - } - - protected function setUp(): void - { - $this->processorProphecy = $this->prophesize(ProcessorInterface::class); - $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $this->resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - } - - public function testOnKernelViewWithControllerResultAndPersist(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->iriConverterProphecy->getIriFromResource($operationResource)->willReturn('/operation_resources/1')->shouldBeCalled(); - $this->resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $this->processorProphecy->process($operationResource, Argument::type(Operation::class), [], Argument::type('array'))->willReturn($operationResource)->shouldBeCalled(); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - '_api_OperationResource_patch' => (new Patch())->withName('_api_OperationResource_patch')->withProcessor('processor'), - '_api_OperationResource_put' => (new Put())->withName('_api_OperationResource_put'), - '_api_OperationResource_post_collection' => (new Post())->withName('_api_OperationResource_post_collection'), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class]); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - foreach (['PATCH', 'PUT', 'POST'] as $httpMethod) { - $request->setMethod($httpMethod); - $request->attributes->set('_api_operation_name', \sprintf('_api_%s_%s%s', 'OperationResource', strtolower($httpMethod), 'POST' === $httpMethod ? '_collection' : '')); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - $this->assertSame($operationResource, $event->getControllerResult()); - $this->assertSame('/operation_resources/1', $request->attributes->get('_api_write_item_iri')); - } - } - - public function testOnKernelViewDoNotCallIriConverterWhenOutputClassDisabled(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->processorProphecy->process($operationResource, Argument::type(Operation::class), [], Argument::type('array'))->willReturn($operationResource)->shouldBeCalled(); - - $this->iriConverterProphecy->getIriFromResource($operationResource)->shouldNotBeCalled(); - $this->resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - 'create_no_output' => (new Post())->withOutput(false)->withName('create_no_output')->withProcessor('processor'), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class, '_api_operation_name' => 'create_no_output']); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testOnKernelViewWithControllerResultAndRemove(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->processorProphecy->process($operationResource, Argument::type(Operation::class), ['identifier' => 1], Argument::type('array'))->willReturn($operationResource)->shouldBeCalled(); - - $this->iriConverterProphecy->getIriFromResource($operationResource)->shouldNotBeCalled(); - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - '_api_OperationResource_delete' => (new Delete())->withUriVariables(['identifier' => (new Link())->withFromClass(OperationResource::class)->withIdentifiers(['identifier'])])->withProcessor('processor')->withName('_api_OperationResource_delete'), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class, '_api_operation_name' => '_api_OperationResource_delete', 'identifier' => 1]); - $request->setMethod('DELETE'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testOnKernelViewWithSafeMethod(): void - { - $operationResource = new OperationResource(1, 'foo'); - $operation = (new Get())->withName('_api_OperationResource_get'); - - $this->processorProphecy->process($operationResource, Argument::type(Operation::class), [], ['operation' => $operation, 'resource_class' => OperationResource::class, 'previous_data' => 'test'])->willReturn($operationResource)->shouldNotBeCalled(); - - $this->iriConverterProphecy->getIriFromResource($operationResource)->shouldNotBeCalled(); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - '_api_OperationResource_get' => $operation, - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class, '_api_operation_name' => '_api_OperationResource_get', 'previous_data' => 'test']); - $request->setMethod('GET'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testDoNotWriteWhenControllerResultIsResponse(): void - { - $this->processorProphecy->process(Argument::cetera())->shouldNotBeCalled(); - - $request = new Request(); - - $response = new Response(); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $response - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testDoNotWriteWhenCant(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->processorProphecy->process(Argument::cetera())->shouldNotBeCalled(); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - 'create_no_write' => (new Post())->withWrite(false), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class, '_api_operation_name' => 'create_no_write']); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testOnKernelViewWithNoResourceClass(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->processorProphecy->process(Argument::cetera())->shouldNotBeCalled(); - - $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource($operationResource)->shouldNotBeCalled(); - - $request = new Request(); - $request->setMethod('POST'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testOnKernelViewInvalidIdentifiers(): void - { - $attributeResource = new AttributeResource(1, 'name'); - - $this->expectException(NotFoundHttpException::class); - $this->expectExceptionMessage('Invalid identifier value or configuration.'); - - $this->processorProphecy->process($attributeResource, Argument::type(Operation::class), ['slug' => 'test'], Argument::type('array'))->shouldNotBeCalled(); - - $this->iriConverterProphecy->getIriFromResource($attributeResource)->shouldNotBeCalled(); - $this->resourceClassResolver->isResourceClass(Argument::type('string'))->shouldNotBeCalled(); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - '_api_OperationResource_delete' => (new Delete())->withUriVariables(['identifier' => (new Link())->withFromClass(OperationResource::class)->withIdentifiers(['identifier'])])->withProcessor('processor')->withName('_api_OperationResource_delete'), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class, '_api_operation_name' => '_api_OperationResource_delete', 'slug' => 'foo']); - $request->setMethod('DELETE'); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $attributeResource - ); - - (new WriteListener($this->processorProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - } - - public function testHasOriginalData(): void - { - $operationResource = new OperationResource(1, 'foo'); - - $this->resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $this->processorProphecy->process($operationResource, Argument::type(Operation::class), [], Argument::type('array'))->willReturn($operationResource)->shouldBeCalled(); - - $operationResourceMetadata = new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations([ - '_api_OperationResource_post_collection' => (new Post())->withName('_api_OperationResource_post_collection'), - ]))]); - - $this->resourceMetadataCollectionFactory->create(OperationResource::class)->willReturn($operationResourceMetadata); - - $request = new Request([], [], ['_api_resource_class' => OperationResource::class]); - - $event = new ViewEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - $request, - HttpKernelInterface::MAIN_REQUEST, - $operationResource - ); - - $request->setMethod('POST'); - $request->attributes->set('_api_operation_name', \sprintf('_api_%s_%s%s', 'OperationResource', 'post', '_collection')); - - (new WriteListener($this->processorProphecy->reveal(), null, $this->resourceClassResolver->reveal(), $this->resourceMetadataCollectionFactory->reveal()))->onKernelView($event); - $this->assertEquals($operationResource, $request->attributes->get('original_data')); - } -} diff --git a/tests/Symfony/Maker/MakeStateProcessorTest.php b/tests/Symfony/Maker/MakeStateProcessorTest.php index 084da7819db..b88cbbce43c 100644 --- a/tests/Symfony/Maker/MakeStateProcessorTest.php +++ b/tests/Symfony/Maker/MakeStateProcessorTest.php @@ -25,7 +25,7 @@ protected function setup(): void (new Filesystem())->remove(self::tempDir()); } - /** @dataProvider stateProcessorProvider */ + #[\PHPUnit\Framework\Attributes\DataProvider('stateProcessorProvider')] public function testMakeStateProcessor(bool $isInteractive): void { $inputs = ['name' => 'CustomStateProcessor']; diff --git a/tests/Symfony/Maker/MakeStateProviderTest.php b/tests/Symfony/Maker/MakeStateProviderTest.php index 8ac538477c3..101dc080082 100644 --- a/tests/Symfony/Maker/MakeStateProviderTest.php +++ b/tests/Symfony/Maker/MakeStateProviderTest.php @@ -25,7 +25,7 @@ protected function setup(): void (new Filesystem())->remove(self::tempDir()); } - /** @dataProvider stateProviderDataProvider */ + #[\PHPUnit\Framework\Attributes\DataProvider('stateProviderDataProvider')] public function testMakeStateProvider(bool $isInteractive): void { $inputs = ['name' => 'CustomStateProvider']; diff --git a/tests/Symfony/Routing/IriConverterTest.php b/tests/Symfony/Routing/IriConverterTest.php index b408f44b545..41d860718b5 100644 --- a/tests/Symfony/Routing/IriConverterTest.php +++ b/tests/Symfony/Routing/IriConverterTest.php @@ -13,21 +13,21 @@ namespace ApiPlatform\Tests\Symfony\Routing; -use ApiPlatform\Api\IdentifiersExtractorInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\NotExposed; use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\State\ProviderInterface; use ApiPlatform\Symfony\Routing\IriConverter; use ApiPlatform\Symfony\Routing\SkolemIriConverter; diff --git a/tests/Symfony/Routing/RouterTest.php b/tests/Symfony/Routing/RouterTest.php index 3ddfb0f8bae..e24b1bbb38c 100644 --- a/tests/Symfony/Routing/RouterTest.php +++ b/tests/Symfony/Routing/RouterTest.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Tests\Symfony\Routing; -use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Symfony\Routing\Router; use PHPUnit\Framework\TestCase; use Prophecy\Argument; diff --git a/tests/Symfony/Security/ResourceAccessCheckerTest.php b/tests/Symfony/Security/ResourceAccessCheckerTest.php index ab586593ac3..c07725cacf5 100644 --- a/tests/Symfony/Security/ResourceAccessCheckerTest.php +++ b/tests/Symfony/Security/ResourceAccessCheckerTest.php @@ -32,9 +32,7 @@ class ResourceAccessCheckerTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider getGranted - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getGranted')] public function testIsGranted(bool $granted): void { $expressionLanguageProphecy = $this->prophesize(ExpressionLanguage::class); diff --git a/tests/Symfony/Validator/EventListener/ValidationExceptionListenerTest.php b/tests/Symfony/Validator/EventListener/ValidationExceptionListenerTest.php deleted file mode 100644 index bf8b9d286c5..00000000000 --- a/tests/Symfony/Validator/EventListener/ValidationExceptionListenerTest.php +++ /dev/null @@ -1,173 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\Validator\EventListener; - -use ApiPlatform\Exception\FilterValidationException; -use ApiPlatform\Symfony\Validator\EventListener\ValidationExceptionListener; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; -use ApiPlatform\Validator\Exception\ValidationException as BaseValidationException; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Intl\Countries; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\ConstraintViolationList; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * @author Kévin Dunglas - */ -class ValidationExceptionListenerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @group legacy - */ - public function testNotValidationException(): void - { - if (!class_exists(Countries::class)) { - $this->markTestSkipped('symfony/intl not installed'); - } - - $listener = new ValidationExceptionListener( - $this->prophesize(SerializerInterface::class)->reveal(), - ['hydra' => ['application/ld+json']]); - - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, new \Exception()); - $listener->onKernelException($event); - $this->assertNull($event->getResponse()); - } - - /** - * @group legacy - */ - public function testValidationException(): void - { - $exceptionJson = '{"foo": "bar"}'; - $list = new ConstraintViolationList([]); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($list, 'hydra', [])->willReturn($exceptionJson)->shouldBeCalled(); - - $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, new ValidationException($list)); - $listener->onKernelException($event); - - $response = $event->getResponse(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame($exceptionJson, $response->getContent()); - $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode()); - $this->assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - } - - /** - * @group legacy - */ - public function testOnKernelValidationExceptionWithCustomStatus(): void - { - $serializedConstraintViolationList = '{"foo": "bar"}'; - $constraintViolationList = new ConstraintViolationList([]); - $exception = new class($constraintViolationList) extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface { - public function __construct(private readonly ConstraintViolationListInterface $constraintViolationList, $message = '', $code = 0, ?\Throwable $previous = null) - { - parent::__construct($message, $code, $previous); - } - - public function getConstraintViolationList(): ConstraintViolationListInterface - { - return $this->constraintViolationList; - } - }; - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($constraintViolationList, 'hydra', [])->willReturn($serializedConstraintViolationList)->shouldBeCalledOnce(); - - $exceptionEvent = new ExceptionEvent( - $this->prophesize(HttpKernelInterface::class)->reveal(), - new Request(), - HttpKernelInterface::MAIN_REQUEST, - $exception - ); - - (new ValidationExceptionListener( - $serializerProphecy->reveal(), - ['hydra' => ['application/ld+json']], - [$exception::class => Response::HTTP_BAD_REQUEST] - ))->onKernelException($exceptionEvent); - - $response = $exceptionEvent->getResponse(); - - self::assertInstanceOf(Response::class, $response); - self::assertSame($serializedConstraintViolationList, $response->getContent()); - self::assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode()); - self::assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type')); - self::assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - self::assertSame('deny', $response->headers->get('X-Frame-Options')); - } - - /** - * @group legacy - */ - public function testValidationFilterException(): void - { - $exceptionJson = '{"message": "my message"}'; - $exception = new FilterValidationException([], 'my message'); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($exception, 'hydra', [])->willReturn($exceptionJson)->shouldBeCalled(); - - $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, $exception); - $listener->onKernelException($event); - - $response = $event->getResponse(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame($exceptionJson, $response->getContent()); - $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode()); - $this->assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - } - - /** - * @group legacy - */ - public function testValidationExceptionWithHydraTitle(): void - { - $exceptionJson = '{"foo": "bar"}'; - $list = new ConstraintViolationList([]); - - $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->serialize($list, 'hydra', ['title' => 'foo'])->willReturn($exceptionJson)->shouldBeCalled(); - - $listener = new ValidationExceptionListener($serializerProphecy->reveal(), ['hydra' => ['application/ld+json']]); - $event = new ExceptionEvent($this->prophesize(HttpKernelInterface::class)->reveal(), new Request(), HttpKernelInterface::MAIN_REQUEST, new ValidationException($list, errorTitle: 'foo')); - $listener->onKernelException($event); - - $response = $event->getResponse(); - $this->assertInstanceOf(Response::class, $response); - $this->assertSame($exceptionJson, $response->getContent()); - $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode()); - $this->assertSame('application/ld+json; charset=utf-8', $response->headers->get('Content-Type')); - $this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options')); - $this->assertSame('deny', $response->headers->get('X-Frame-Options')); - } -} diff --git a/tests/Symfony/Validator/Exception/ValidationExceptionTest.php b/tests/Symfony/Validator/Exception/ValidationExceptionTest.php deleted file mode 100644 index cb9f9cae154..00000000000 --- a/tests/Symfony/Validator/Exception/ValidationExceptionTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Symfony\Validator\Exception; - -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; -use ApiPlatform\Validator\Exception\ValidationException as MainValidationException; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\ConstraintViolationList; - -/** - * @author Kévin Dunglas - */ -class ValidationExceptionTest extends TestCase -{ - public function testToString(): void - { - $e = new ValidationException(new ConstraintViolationList([ - new ConstraintViolation('message 1', '', [], '', '', 'invalid'), - new ConstraintViolation('message 2', '', [], '', 'foo', 'invalid'), - ])); - $this->assertInstanceOf(MainValidationException::class, $e); - $this->assertInstanceOf(RuntimeException::class, $e); - $this->assertInstanceOf(\RuntimeException::class, $e); - - $this->assertSame(str_replace(\PHP_EOL, "\n", <<__toString()); - } -} diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaChoiceRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaChoiceRestrictionTest.php index 554a1da6cb3..476f5394969 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaChoiceRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaChoiceRestrictionTest.php @@ -36,9 +36,7 @@ protected function setUp(): void $this->propertySchemaChoiceRestriction = new PropertySchemaChoiceRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaChoiceRestriction->supports($constraint, $propertyMetadata)); @@ -59,9 +57,7 @@ public static function supportsProvider(): \Generator yield 'not supported type' => [new Choice(['choices' => [new \stdClass(), new \stdClass()]]), (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT)]), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaChoiceRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCollectionRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCollectionRestrictionTest.php index c0f187d4014..e7b961c4033 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCollectionRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCollectionRestrictionTest.php @@ -54,9 +54,7 @@ protected function setUp(): void ]); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaCollectionRestriction->supports($constraint, $propertyMetadata)); @@ -69,9 +67,7 @@ public static function supportsProvider(): \Generator yield 'not supported' => [new Positive(), new ApiProperty(), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertEquals($expectedResult, $this->propertySchemaCollectionRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCountRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCountRestrictionTest.php index ca6e65aa15f..b4a14957394 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCountRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaCountRestrictionTest.php @@ -35,9 +35,7 @@ protected function setUp(): void $this->propertySchemaCountRestriction = new PropertySchemaCountRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaCountRestriction->supports($constraint, $propertyMetadata)); @@ -49,9 +47,7 @@ public static function supportsProvider(): \Generator yield 'not supported' => [new Positive(), new ApiProperty(), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaCountRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormatTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormatTest.php index b76db0761b9..41a078df582 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormatTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaFormatTest.php @@ -37,9 +37,7 @@ protected function setUp(): void $this->propertySchemaFormatRestriction = new PropertySchemaFormat(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaFormatRestriction->supports($constraint, $propertyMetadata)); @@ -60,9 +58,7 @@ public static function supportsProvider(): \Generator yield 'not supported' => [new Positive(), new ApiProperty(), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaFormatRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanOrEqualRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanOrEqualRestrictionTest.php index 3411366660d..5d42f79540e 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanOrEqualRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanOrEqualRestrictionTest.php @@ -37,9 +37,7 @@ protected function setUp(): void $this->propertySchemaGreaterThanOrEqualRestriction = new PropertySchemaGreaterThanOrEqualRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaGreaterThanOrEqualRestriction->supports($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanRestrictionTest.php index 73901abef57..f348b951bb4 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaGreaterThanRestrictionTest.php @@ -37,9 +37,7 @@ protected function setUp(): void $this->propertySchemaGreaterThanRestriction = new PropertySchemaGreaterThanRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaGreaterThanRestriction->supports($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanOrEqualRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanOrEqualRestrictionTest.php index c11f7289b0e..fcab413d95b 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanOrEqualRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanOrEqualRestrictionTest.php @@ -37,9 +37,7 @@ protected function setUp(): void $this->propertySchemaLessThanOrEqualRestriction = new PropertySchemaLessThanOrEqualRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaLessThanOrEqualRestriction->supports($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanRestrictionTest.php index 80409cd3a5f..ad2b942e9ee 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaLessThanRestrictionTest.php @@ -37,9 +37,7 @@ protected function setUp(): void $this->propertySchemaLessThanRestriction = new PropertySchemaLessThanRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaLessThanRestriction->supports($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaOneOfRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaOneOfRestrictionTest.php index 5bed917c5e9..45b65d1165d 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaOneOfRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaOneOfRestrictionTest.php @@ -42,9 +42,7 @@ protected function setUp(): void ]); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaOneOfRestriction->supports($constraint, $propertyMetadata)); @@ -60,9 +58,7 @@ public static function supportsProvider(): \Generator yield 'not supported' => [new Positive(), new ApiProperty(), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaOneOfRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php index 19ec7b816e6..8d56c342c1e 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRangeRestrictionTest.php @@ -36,9 +36,7 @@ protected function setUp(): void $this->propertySchemaRangeRestriction = new PropertySchemaRangeRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaRangeRestriction->supports($constraint, $propertyMetadata)); @@ -54,9 +52,7 @@ public static function supportsProvider(): \Generator yield 'not supported type' => [new Range(['min' => 1]), (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaRangeRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestrictionTest.php index 396a7569c2a..4066ee77938 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRegexRestrictionTest.php @@ -35,9 +35,7 @@ protected function setUp(): void $this->propertySchemaRegexRestriction = new PropertySchemaRegexRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaRegexRestriction->supports($constraint, $propertyMetadata)); @@ -50,9 +48,7 @@ public static function supportsProvider(): \Generator yield 'not supported' => [new Positive(), new ApiProperty(), false]; } - /** - * @dataProvider createProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createProvider')] public function testCreate(Constraint $constraint, ApiProperty $propertyMetadata, array $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaRegexRestriction->create($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php index 1c84d8b7fb5..76fc276a01e 100644 --- a/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php +++ b/tests/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaUniqueRestrictionTest.php @@ -35,9 +35,7 @@ protected function setUp(): void $this->propertySchemaUniqueRestriction = new PropertySchemaUniqueRestriction(); } - /** - * @dataProvider supportsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('supportsProvider')] public function testSupports(Constraint $constraint, ApiProperty $propertyMetadata, bool $expectedResult): void { self::assertSame($expectedResult, $this->propertySchemaUniqueRestriction->supports($constraint, $propertyMetadata)); diff --git a/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php b/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php index cac0539a19a..78b8231798a 100644 --- a/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php +++ b/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php @@ -318,9 +318,7 @@ public function testCreateWithPropertyRegexRestriction(): void $this->assertEquals('^(dummy)$', $schema['pattern']); } - /** - * @dataProvider providePropertySchemaFormatCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('providePropertySchemaFormatCases')] public function testCreateWithPropertyFormatRestriction(string $property, string $class, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata($class); @@ -471,9 +469,7 @@ public function testCreateWithPropertyUniqueRestriction(): void $this->assertEquals(['uniqueItems' => true], $schema); } - /** - * @dataProvider provideRangeConstraintCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideRangeConstraintCases')] public function testCreateWithRangeConstraint(Type $type, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyRangeValidatedEntity::class); @@ -508,9 +504,7 @@ public static function provideRangeConstraintCases(): \Generator yield 'min/max float' => ['type' => new Type(Type::BUILTIN_TYPE_FLOAT), 'property' => 'dummyFloatMinMax', 'expectedSchema' => ['minimum' => 1.5, 'maximum' => 10.5]]; } - /** - * @dataProvider provideChoiceConstraintCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideChoiceConstraintCases')] public function testCreateWithPropertyChoiceRestriction(ApiProperty $propertyMetadata, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyValidatedChoiceEntity::class); @@ -547,9 +541,7 @@ public static function provideChoiceConstraintCases(): \Generator yield 'multi choice min/max' => ['propertyMetadata' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]), 'property' => 'dummyMultiChoiceMinMax', 'expectedSchema' => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ['a', 'b', 'c', 'd']], 'minItems' => 2, 'maxItems' => 4]]; } - /** - * @dataProvider provideCountConstraintCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideCountConstraintCases')] public function testCreateWithPropertyCountRestriction(string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyCountValidatedEntity::class); @@ -651,9 +643,7 @@ public function testCreateWithPropertyCollectionRestriction(): void ], $schema); } - /** - * @dataProvider provideNumericConstraintCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideNumericConstraintCases')] public function testCreateWithPropertyNumericRestriction(ApiProperty $propertyMetadata, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyNumericValidatedEntity::class); diff --git a/tests/Symfony/Validator/ValidatorTest.php b/tests/Symfony/Validator/ValidatorTest.php index 189bd85691f..c678469a66e 100644 --- a/tests/Symfony/Validator/ValidatorTest.php +++ b/tests/Symfony/Validator/ValidatorTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Tests\Symfony\Validator; -use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; use ApiPlatform\Symfony\Validator\Validator; use ApiPlatform\Tests\Fixtures\DummyEntity; @@ -44,7 +43,7 @@ public function testValid(): void $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator, legacyValidationException: false); + $validator = new Validator($symfonyValidator); $validator->validate(new DummyEntity()); } @@ -59,25 +58,7 @@ public function testInvalid(): void $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationList)->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator, legacyValidationException: false); - $validator->validate(new DummyEntity()); - } - - /** - * @group legacy - */ - public function testDeprecatedInvalid(): void - { - $this->expectException(LegacyValidationException::class); - - $data = new DummyEntity(); - $constraintViolationList = new ConstraintViolationList([new ConstraintViolation('test', null, [], null, 'test', null), new ConstraintViolation('test', null, [], null, 'test', null)]); - - $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class); - $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationList)->shouldBeCalled(); - $symfonyValidator = $symfonyValidatorProphecy->reveal(); - - $validator = new Validator($symfonyValidator, legacyValidationException: true); + $validator = new Validator($symfonyValidator); $validator->validate(new DummyEntity()); } @@ -93,7 +74,7 @@ public function testGetGroupsFromCallable(): void $symfonyValidatorProphecy->validate($data, null, $expectedValidationGroups)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator, legacyValidationException: false); + $validator = new Validator($symfonyValidator); $validator->validate(new DummyEntity(), ['groups' => fn ($data): array => $data instanceof DummyEntity ? $expectedValidationGroups : []]); } @@ -116,7 +97,7 @@ public function __invoke(object $object): array } }); - $validator = new Validator($symfonyValidatorProphecy->reveal(), $containerProphecy->reveal(), legacyValidationException: false); + $validator = new Validator($symfonyValidatorProphecy->reveal(), $containerProphecy->reveal()); $validator->validate(new DummyEntity(), ['groups' => 'groups_builder']); } @@ -135,7 +116,7 @@ public function testValidatorWithScalarGroup(): void $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has('foo')->willReturn(false)->shouldBeCalled(); - $validator = new Validator($symfonyValidator, $containerProphecy->reveal(), legacyValidationException: false); + $validator = new Validator($symfonyValidator, $containerProphecy->reveal()); $validator->validate(new DummyEntity(), ['groups' => 'foo']); } } diff --git a/tests/TestSuiteConfigCache.php b/tests/TestSuiteConfigCache.php new file mode 100644 index 00000000000..e26db511af0 --- /dev/null +++ b/tests/TestSuiteConfigCache.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +use Symfony\Component\Config\ConfigCacheInterface; + +final class TestSuiteConfigCache implements ConfigCacheInterface +{ + /** @var array */ + public static array $hashes = []; + + public function __construct(private readonly ConfigCacheInterface $decorated) + { + } + + public function getPath(): string + { + return $this->decorated->getPath(); + } + + public function isFresh(): bool + { + $p = $this->getPath(); + if (!isset(self::$hashes[$p]) || self::$hashes[$p] !== $this->getHash()) { + self::$hashes[$p] = $this->getHash(); + + return false; + } + + return $this->decorated->isFresh(); + } + + public function write(string $content, ?array $metadata = null): void + { + $this->decorated->write($content, $metadata); + } + + private function getHash(): string + { + return hash_file('xxh3', __DIR__.'/Fixtures/app/var/resources.php'); + } +} diff --git a/tests/Util/ErrorFormatGuesserTest.php b/tests/Util/ErrorFormatGuesserTest.php deleted file mode 100644 index e7f7b91c2bf..00000000000 --- a/tests/Util/ErrorFormatGuesserTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Tests\Util; - -use ApiPlatform\Util\ErrorFormatGuesser; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Request; - -/** - * @author Kévin Dunglas - */ -class ErrorFormatGuesserTest extends TestCase -{ - public function testGuessErrorFormat(): void - { - $request = new Request(); - $request->setRequestFormat('jsonld'); - - $format = ErrorFormatGuesser::guessErrorFormat($request, ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); - $this->assertSame('jsonld', $format['key']); - $this->assertSame('application/ld+json', $format['value'][0]); - } - - public function testFallback(): void - { - $format = ErrorFormatGuesser::guessErrorFormat(new Request(), ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); - $this->assertSame('xml', $format['key']); - $this->assertSame('text/xml', $format['value'][0]); - } - - public function testFallbackWhenNotSupported(): void - { - $request = new Request(); - $request->setRequestFormat('html'); - - $format = ErrorFormatGuesser::guessErrorFormat($request, ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); - $this->assertSame('xml', $format['key']); - $this->assertSame('text/xml', $format['value'][0]); - } - - public function testGuessCustomErrorFormat(): void - { - $request = new Request(); - $request->setRequestFormat('custom_json_format'); - - $format = ErrorFormatGuesser::guessErrorFormat($request, ['xml' => ['text/xml'], 'custom_json_format' => ['application/json']]); - $this->assertSame('custom_json_format', $format['key']); - $this->assertSame('application/json', $format['value'][0]); - } -} diff --git a/tests/WithResourcesTrait.php b/tests/WithResourcesTrait.php new file mode 100644 index 00000000000..1b4b8756e29 --- /dev/null +++ b/tests/WithResourcesTrait.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests; + +trait WithResourcesTrait +{ + /** + * @param class-string[] $resources + */ + protected static function writeResources(array $resources): void + { + file_put_contents(__DIR__.'/Fixtures/app/var/resources.php', \sprintf(' $v.'::class', $resources)))); + } + + protected static function removeResources(): void + { + file_put_contents(__DIR__.'/Fixtures/app/var/resources.php', ' + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +$opts = [ + 'http' => [ + 'method' => 'GET', + 'header' => "User-Agent: Mozilla/5.0\r\n", + ], +]; + +$context = stream_context_create($opts); +$hydraContext = json_decode(file_get_contents('http://www.w3.org/ns/hydra/context.jsonld', false, $context), true); +file_put_contents('src/JsonLd/HydraContext.php', '