diff --git a/.github/workflows/ci-code.yml b/.github/workflows/ci-code.yml index 97d20f988..272b83e6a 100644 --- a/.github/workflows/ci-code.yml +++ b/.github/workflows/ci-code.yml @@ -56,4 +56,5 @@ jobs: - run: composer phpcs - run: composer phpstan + - run: composer inference - run: bin/console lint:mixin diff --git a/REUSE.toml b/REUSE.toml index b4829af26..4dce1d282 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,6 +1,6 @@ version = 1 [[annotations]] -path = [ "*.yml", "*.yaml", ".git*", "*.dist", "docs/.pages", "docs/validators/.pages", "composer.json", "composer.lock", "tests/fixtures/*", ".github/*.yml", ".github/actions/**.yml", ".github/workflows/**.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/ISSUE_TEMPLATE/**" ] +path = [ "fluent.neon", "*.yml", "*.yaml", ".git*", "*.dist", "docs/.pages", "docs/validators/.pages", "composer.json", "composer.lock", "tests/fixtures/*", ".github/*.yml", ".github/actions/**.yml", ".github/workflows/**.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/ISSUE_TEMPLATE/**" ] SPDX-FileCopyrightText = "Respect Project Contributors" SPDX-License-Identifier = "MIT" diff --git a/composer.json b/composer.json index 0177f54d2..12cd73a86 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "php": ">=8.5", "php-di/php-di": "^7.1", "psr/container": "^2.0", + "respect/fluent": "^2.0", "respect/string-formatter": "^1.7", "respect/stringifier": "^3.0", "symfony/polyfill-intl-idn": "^1.33", @@ -36,13 +37,15 @@ "pestphp/pest": "^4.2", "phpbench/phpbench": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "^2.1", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^12.5", "psr/http-message": "^1.0 || ^2.0", "ramsey/uuid": "^4", "respect/coding-standard": "^5.0", + "respect/fluent-analysis": "^2.0", + "respect/fluentgen": "^2.0", "sebastian/diff": "^7.0", "sokil/php-isocodes": "^4.2.1", "sokil/php-isocodes-db-only": "^4.0", @@ -61,6 +64,11 @@ "sokil/php-isocodes": "Enable rules that validate ISO codes", "sokil/php-isocodes-db-only": "Enable rules that validate ISO codes" }, + "extra": { + "phpstan": { + "includes": ["fluent.neon"] + } + }, "autoload": { "psr-4": { "Respect\\Validation\\": "src/" @@ -71,7 +79,8 @@ "psr-4": { "Respect\\Dev\\": "src-dev/", "Respect\\Validation\\": "tests/unit/", - "Respect\\Validation\\Test\\": "tests/src/" + "Respect\\Validation\\Test\\": "tests/src/", + "Respect\\Validation\\Test\\Inference\\": "tests/inference/" } }, "scripts": { @@ -84,6 +93,7 @@ "phpcs": "vendor/bin/phpcs", "phpstan": "vendor/bin/phpstan analyze", "phpunit": "vendor/bin/phpunit --testsuite=unit", + "inference": "vendor/bin/phpunit --testsuite=inference", "smoke-complete": "bin/console smoke-tests:check-complete", "spdx-lint": "bin/console lint:spdx", "qa": [ @@ -92,6 +102,7 @@ "@phpstan", "@phpunit", "@pest", + "@inference", "@docs", "@smoke-complete" ] diff --git a/composer.lock b/composer.lock index 8a5920080..81b98b6da 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9851274a86bdca1f0835e29e06bd8858", + "content-hash": "0e648fd112253307245f3508fa199796", "packages": [ { "name": "laravel/serializable-closure", - "version": "v2.0.9", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { @@ -65,7 +65,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-02-03T06:55:34+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { "name": "php-di/invoker", @@ -248,6 +248,59 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "respect/fluent", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Respect/Fluent.git", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/Fluent/zipball/f32c76e37a82a9e63d6fe700a27201534f72da60", + "reference": "f32c76e37a82a9e63d6fe700a27201534f72da60", + "shasum": "" + }, + "require": { + "php": "^8.5" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Respect\\Fluent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Respect/Fluent Contributors", + "homepage": "https://github.com/Respect/Fluent/graphs/contributors" + } + ], + "description": "Namespace-aware fluent class resolution", + "keywords": [ + "builder", + "fluent", + "respect" + ], + "support": { + "issues": "https://github.com/Respect/Fluent/issues", + "source": "https://github.com/Respect/Fluent/tree/2.0.1" + }, + "time": "2026-03-26T04:24:51+00:00" + }, { "name": "respect/string-formatter", "version": "1.7.0", @@ -706,16 +759,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.19.0", + "version": "v7.19.2", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6" + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", - "reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", "shasum": "" }, "require": { @@ -729,9 +782,9 @@ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", "phpunit/php-file-iterator": "^6.0.1 || ^7", "phpunit/php-timer": "^8 || ^9", - "phpunit/phpunit": "^12.5.9 || ^13", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", "sebastian/environment": "^8.0.3 || ^9", - "symfony/console": "^7.4.4 || ^8.0.4", + "symfony/console": "^7.4.7 || ^8.0.7", "symfony/process": "^7.4.5 || ^8.0.5" }, "require-dev": { @@ -739,11 +792,11 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.38", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.12", - "phpstan/phpstan-strict-rules": "^2.0.8", - "symfony/filesystem": "^7.4.0 || ^8.0.1" + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" }, "bin": [ "bin/paratest", @@ -783,7 +836,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.19.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" }, "funding": [ { @@ -795,20 +848,20 @@ "type": "paypal" } ], - "time": "2026-02-06T10:53:26+00:00" + "time": "2026-03-09T14:33:17+00:00" }, { "name": "brick/math", - "version": "0.14.7", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/07ff363b16ef8aca9692bba3be9e73fe63f34e50", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -847,7 +900,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.7" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -855,7 +908,7 @@ "type": "github" } ], - "time": "2026-02-07T10:57:35+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -2068,20 +2121,20 @@ }, { "name": "pestphp/pest", - "version": "v4.4.2", + "version": "v4.4.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", - "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e6ab897594312728ef2e32d586cb4f6780b1b495", + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495", "shasum": "" }, "require": { - "brianium/paratest": "^7.19.0", + "brianium/paratest": "^7.19.2", "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", @@ -2089,12 +2142,12 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.12", + "phpunit/phpunit": "^12.5.14", "symfony/process": "^7.4.5|^8.0.5" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.12", + "phpunit/phpunit": ">12.5.14", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -2168,7 +2221,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.4.2" + "source": "https://github.com/pestphp/pest/tree/v4.4.3" }, "funding": [ { @@ -2180,7 +2233,7 @@ "type": "github" } ], - "time": "2026-03-10T21:09:12+00:00" + "time": "2026-03-21T13:14:39+00:00" }, { "name": "pestphp/pest-plugin", @@ -2625,16 +2678,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.5.1", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/661c8c6abbc7734986cf7bc6062c237fbb450461", + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461", "shasum": "" }, "require": { @@ -2712,7 +2765,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.5.1" + "source": "https://github.com/phpbench/phpbench/tree/1.6.1" }, "funding": [ { @@ -2720,7 +2773,7 @@ "type": "github" } ], - "time": "2026-03-05T08:18:58+00:00" + "time": "2026-03-22T10:27:20+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2777,16 +2830,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -2836,9 +2889,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2995,11 +3048,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.42", + "version": "2.1.44", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", - "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218", + "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218", "shasum": "" }, "require": { @@ -3044,7 +3097,7 @@ "type": "github" } ], - "time": "2026-03-17T14:58:32+00:00" + "time": "2026-03-25T17:34:21+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -3500,16 +3553,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.12", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/418e06b3b46b0d54bad749ff4907fc7dfb530199", - "reference": "418e06b3b46b0d54bad749ff4907fc7dfb530199", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { @@ -3578,7 +3631,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -3602,7 +3655,7 @@ "type": "tidelift" } ], - "time": "2026-02-16T08:34:36+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { "name": "psr/cache", @@ -4004,6 +4057,115 @@ }, "time": "2026-01-19T10:34:07+00:00" }, + { + "name": "respect/fluent-analysis", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/Respect/FluentAnalysis.git", + "reference": "c290b858838b1e71cec2d7f2dec72b0e6b82ed49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/FluentAnalysis/zipball/c290b858838b1e71cec2d7f2dec72b0e6b82ed49", + "reference": "c290b858838b1e71cec2d7f2dec72b0e6b82ed49", + "shasum": "" + }, + "require": { + "php": "^8.5", + "phpstan/phpstan": "^2.1", + "respect/fluent": "^2.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0" + }, + "bin": [ + "bin/fluent-analysis" + ], + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Respect\\FluentAnalysis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "description": "PHPStan extension for Respect/Fluent builder method resolution", + "support": { + "issues": "https://github.com/Respect/FluentAnalysis/issues", + "source": "https://github.com/Respect/FluentAnalysis/tree/2.0.1" + }, + "time": "2026-03-26T04:27:08+00:00" + }, + { + "name": "respect/fluentgen", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/Respect/FluentGen.git", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Respect/FluentGen/zipball/6a9065516f403c5f5abc86646290bd08e44c538e", + "reference": "6a9065516f403c5f5abc86646290bd08e44c538e", + "shasum": "" + }, + "require": { + "nette/php-generator": "^4.1", + "php": "^8.5" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^12.5", + "respect/coding-standard": "^5.0", + "respect/fluent": "^2.0" + }, + "suggest": { + "respect/fluent": "Enables #[Composable] prefix composition support" + }, + "type": "library", + "autoload": { + "psr-4": { + "Respect\\FluentGen\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Respect/FluentGen Contributors", + "homepage": "https://github.com/Respect/FluentGen/graphs/contributors" + } + ], + "description": "Code generation for fluent builder interfaces", + "keywords": [ + "fluent", + "fluentgen", + "mixin", + "respect" + ], + "support": { + "issues": "https://github.com/Respect/FluentGen/issues", + "source": "https://github.com/Respect/FluentGen/tree/2.0.0" + }, + "time": "2026-03-25T05:50:09+00:00" + }, { "name": "sebastian/cli-parser", "version": "4.2.0", @@ -4292,16 +4454,16 @@ }, { "name": "sebastian/environment", - "version": "8.0.3", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", - "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { @@ -4344,7 +4506,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { @@ -4364,7 +4526,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T14:11:56+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", @@ -4967,32 +5129,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.27.1", + "version": "8.28.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769" + "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/29bdaee8b65e7ed2b8e702b01852edba8bae1769", - "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/66151cfbd25b50e8becd9f809fb704f01fd4d6f2", + "reference": "66151cfbd25b50e8becd9f809fb704f01fd4d6f2", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.3.1", + "phpstan/phpdoc-parser": "^2.3.2", "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phing/phing": "3.0.1|3.1.1", + "phing/phing": "3.0.1|3.1.2", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.37", - "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.12", - "phpstan/phpstan-strict-rules": "2.0.7", - "phpunit/phpunit": "9.6.31|10.5.60|11.4.4|11.5.49|12.5.7" + "phpstan/phpstan": "2.1.42", + "phpstan/phpstan-deprecation-rules": "2.0.4", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.10", + "phpunit/phpunit": "9.6.34|10.5.63|11.4.4|11.5.50|12.5.14" }, "type": "phpcodesniffer-standard", "extra": { @@ -5016,7 +5178,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.27.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.28.1" }, "funding": [ { @@ -5028,7 +5190,7 @@ "type": "tidelift" } ], - "time": "2026-01-25T15:57:07+00:00" + "time": "2026-03-22T17:22:38+00:00" }, { "name": "sokil/php-isocodes", @@ -5264,39 +5426,47 @@ }, { "name": "symfony/console", - "version": "v8.0.7", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.0", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.4|^8.0" + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/lock": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5330,7 +5500,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.7" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -5350,7 +5520,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:22+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/deprecation-contracts", diff --git a/fluent.neon b/fluent.neon new file mode 100644 index 000000000..e2b468514 --- /dev/null +++ b/fluent.neon @@ -0,0 +1,4 @@ +parameters: + fluent: + builders: + - builder: Respect\Validation\ValidatorBuilder diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 20d17139b..f00329292 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -23,6 +23,10 @@ tests/Pest.php + tests/inference/ + + + tests/inference/ src/Mixins/ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9a0d4638f..734c91938 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - fluent.neon + parameters: fileExtensions: - php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 51339be5c..8dae82703 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,9 @@ ./tests/feature + + tests/inference/ + diff --git a/src-dev/CodeGen/Config.php b/src-dev/CodeGen/Config.php deleted file mode 100644 index af3a8864f..000000000 --- a/src-dev/CodeGen/Config.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class Config -{ - public function __construct( - public string $sourceDir, - public string $sourceNamespace, - public string $outputDir, - public string $outputNamespace, - public OutputFormatter $outputFormatter = new OutputFormatter(), - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php b/src-dev/CodeGen/FluentBuilder/MethodBuilder.php deleted file mode 100644 index 72da85787..000000000 --- a/src-dev/CodeGen/FluentBuilder/MethodBuilder.php +++ /dev/null @@ -1,169 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\Method; -use Nette\PhpGenerator\PhpNamespace; -use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionUnionType; - -use function count; -use function implode; -use function in_array; -use function is_object; -use function lcfirst; -use function preg_replace; -use function sort; -use function str_starts_with; -use function ucfirst; - -final class MethodBuilder -{ - /** - * @param array $excludedTypePrefixes - * @param array $excludedTypeNames - */ - public function __construct( - private readonly array $excludedTypePrefixes = [], - private readonly array $excludedTypeNames = [], - ) { - } - - public function build( - PhpNamespace $namespace, - ReflectionClass $nodeReflection, - string $returnType, - string|null $prefix = null, - bool $static = false, - ReflectionParameter|null $prefixParameter = null, - ): Method { - $originalName = $nodeReflection->getShortName(); - $name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName); - - $method = new Method($name); - $method->setPublic()->setReturnType($returnType); - - if ($static) { - $method->setStatic(); - } - - if ($prefixParameter !== null) { - $this->addPrefixParameter($method, $prefixParameter); - } - - $constructor = $nodeReflection->getConstructor(); - if ($constructor === null) { - return $method; - } - - $comment = $constructor->getDocComment(); - if ($comment) { - $method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment)); - } - - foreach ($constructor->getParameters() as $reflectionParameter) { - $this->addParameter($method, $reflectionParameter, $namespace); - } - - return $method; - } - - private function addPrefixParameter(Method $method, ReflectionParameter $reflectionParameter): void - { - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - } - - sort($types); - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - } - - $method->addParameter($reflectionParameter->getName())->setType(implode('|', $types)); - } - - private function addParameter( - Method $method, - ReflectionParameter $reflectionParameter, - PhpNamespace $namespace, - ): void { - if ($reflectionParameter->isVariadic()) { - $method->setVariadic(); - } - - $type = $reflectionParameter->getType(); - $types = []; - - if ($type instanceof ReflectionUnionType) { - foreach ($type->getTypes() as $subType) { - $types[] = $subType->getName(); - if ($subType->isBuiltin()) { - continue; - } - - $namespace->addUse($subType->getName()); - } - } elseif ($type instanceof ReflectionNamedType) { - $types[] = $type->getName(); - - if ($this->isExcludedType($type->getName())) { - return; - } - - if (!$type->isBuiltin()) { - $namespace->addUse($type->getName()); - } - } - - $parameter = $method->addParameter($reflectionParameter->getName()); - $parameter->setType(implode('|', $types)); - - if (!$reflectionParameter->isDefaultValueAvailable()) { - $parameter->setNullable($reflectionParameter->isOptional()); - } - - if (count($types) > 1 || $reflectionParameter->isVariadic()) { - $parameter->setNullable(false); - } - - if (!$reflectionParameter->isDefaultValueAvailable()) { - return; - } - - $defaultValue = $reflectionParameter->getDefaultValue(); - if (is_object($defaultValue)) { - $parameter->setDefaultValue(null); - $parameter->setNullable(true); - - return; - } - - $parameter->setDefaultValue($defaultValue); - $parameter->setNullable(false); - } - - private function isExcludedType(string $typeName): bool - { - foreach ($this->excludedTypePrefixes as $excludedPrefix) { - if (str_starts_with($typeName, $excludedPrefix)) { - return true; - } - } - - return in_array($typeName, $this->excludedTypeNames, true); - } -} diff --git a/src-dev/CodeGen/FluentBuilder/Mixin.php b/src-dev/CodeGen/FluentBuilder/Mixin.php deleted file mode 100644 index 0e211711c..000000000 --- a/src-dev/CodeGen/FluentBuilder/Mixin.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Attribute; - -#[Attribute(Attribute::TARGET_CLASS)] -final readonly class Mixin -{ - /** - * @param array $exclude - * @param array $include - */ - public function __construct( - public string|null $prefix = null, - public bool $prefixParameter = false, - public bool $requireInclusion = false, - public array $exclude = [], - public array $include = [], - ) { - } -} diff --git a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php b/src-dev/CodeGen/FluentBuilder/MixinGenerator.php deleted file mode 100644 index 3f605873a..000000000 --- a/src-dev/CodeGen/FluentBuilder/MixinGenerator.php +++ /dev/null @@ -1,253 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use ReflectionParameter; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\InterfaceConfig; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function file_get_contents; -use function in_array; -use function is_file; -use function is_readable; -use function ksort; - -final class MixinGenerator implements CodeGenerator -{ - /** @param array $interfaces */ - public function __construct( - private readonly Config $config, - private readonly MethodBuilder $methodBuilder = new MethodBuilder(), - private readonly array $interfaces = [], - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $filters = $this->discoverFilters($nodes); - - $files = []; - - foreach ($this->interfaces as $interfaceConfig) { - $prefixInterfaceNames = []; - - foreach ($prefixes as $prefix) { - $interfaceName = $prefix['name'] . $interfaceConfig->suffix; - $prefixInterfaceNames[] = $this->config->outputNamespace . '\\' . $interfaceName; - - $this->generateInterface( - $interfaceName, - $interfaceConfig, - $nodes, - $filters, - $prefix, - $files, - ); - } - - $this->generateRootInterface( - $interfaceConfig, - $prefixInterfaceNames, - $nodes, - $filters, - $files, - ); - } - - return $files; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $constructor = $reflection->getConstructor(); - $prefixParameter = null; - - if ($mixin->prefixParameter && $constructor !== null) { - $parameters = $constructor->getParameters(); - if ($parameters !== []) { - $prefixParameter = $parameters[0]; - } - } - - $prefixes[$mixin->prefix] = [ - 'name' => $reflection->getShortName(), - 'prefix' => $mixin->prefix, - 'requireInclusion' => $mixin->requireInclusion, - 'prefixParameter' => $prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverFilters(array $nodes): array - { - $filters = []; - - foreach ($nodes as $name => $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $filters[$name] = $attributes[0]->newInstance(); - } - - return $filters; - } - - /** - * @param array $nodes - * @param array $filters - * @param array{name: string, prefix: string, requireInclusion: bool, prefixParameter: ?ReflectionParameter} $prefix - * @param array $files - */ - private function generateInterface( - string $interfaceName, - InterfaceConfig $config, - array $nodes, - array $filters, - array $prefix, - array &$files, - ): void { - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($nodes as $name => $reflection) { - $mixin = $filters[$name] ?? null; - - if ($prefix['requireInclusion']) { - if ($mixin === null || !in_array($prefix['prefix'], $mixin->include, true)) { - continue; - } - } elseif ($mixin !== null && in_array($prefix['prefix'], $mixin->exclude, true)) { - continue; - } - - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - $prefix['prefix'], - $config->static, - $prefix['prefixParameter'], - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** - * @param array $prefixInterfaceNames - * @param array $nodes - * @param array $filters - * @param array $files - */ - private function generateRootInterface( - InterfaceConfig $config, - array $prefixInterfaceNames, - array $nodes, - array $filters, - array &$files, - ): void { - $interfaceName = $config->suffix; - $namespace = new PhpNamespace($this->config->outputNamespace); - $interface = $namespace->addInterface($interfaceName); - - foreach ($config->rootExtends as $extend) { - $namespace->addUse($extend); - $interface->addExtend($extend); - } - - foreach ($prefixInterfaceNames as $prefixInterfaceName) { - $namespace->addUse($prefixInterfaceName); - $interface->addExtend($prefixInterfaceName); - } - - if ($config->rootComment !== null) { - $interface->addComment($config->rootComment); - } - - foreach ($config->rootUses as $use) { - $namespace->addUse($use); - } - - foreach ($nodes as $reflection) { - $method = $this->methodBuilder->build( - $namespace, - $reflection, - $config->returnType, - null, - $config->static, - ); - - $interface->addMember($method); - } - - $this->addFile($interfaceName, $namespace, $files); - } - - /** @param array $files */ - private function addFile(string $interfaceName, PhpNamespace $namespace, array &$files): void - { - $filename = $this->config->outputDir . '/' . $interfaceName . '.php'; - - $printer = new Printer(); - $printer->wrapLength = 300; - - $existingContent = ''; - if (is_file($filename) && is_readable($filename)) { - $existingContent = file_get_contents($filename) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - $files[$filename] = $formattedContent; - } -} diff --git a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php b/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php deleted file mode 100644 index a91a9c63f..000000000 --- a/src-dev/CodeGen/FluentBuilder/PrefixMapGenerator.php +++ /dev/null @@ -1,160 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen\FluentBuilder; - -use Nette\PhpGenerator\PhpNamespace; -use Nette\PhpGenerator\Printer; -use ReflectionClass; -use Respect\Dev\CodeGen\CodeGenerator; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\NamespaceScanner; - -use function array_keys; -use function ctype_upper; -use function file_get_contents; -use function is_file; -use function is_readable; -use function ksort; -use function lcfirst; -use function str_starts_with; -use function strlen; -use function uksort; - -final class PrefixMapGenerator implements CodeGenerator -{ - public function __construct( - private readonly Config $config, - private readonly string $outputClassName, - ) { - } - - /** @return array filename => content */ - public function generate(): array - { - $nodes = NamespaceScanner::scan($this->config->sourceDir, $this->config->sourceNamespace); - $prefixes = $this->discoverPrefixes($nodes); - $composable = $this->buildComposable($nodes, $prefixes); - $composableWithArgument = $this->buildComposableWithArgument($prefixes); - - $namespace = new PhpNamespace($this->config->outputNamespace); - $class = $namespace->addClass($this->outputClassName); - $class->setFinal(); - - $class->addConstant('COMPOSABLE', $composable)->setPublic()->setType('array'); - $class->addConstant('COMPOSABLE_WITH_ARGUMENT', $composableWithArgument)->setPublic()->setType('array'); - - $printer = new Printer(); - $printer->wrapLength = 300; - - $outputFile = $this->config->outputDir . '/' . $this->outputClassName . '.php'; - - $existingContent = ''; - if (is_file($outputFile) && is_readable($outputFile)) { - $existingContent = file_get_contents($outputFile) ?: ''; - } - - $formattedContent = $this->config->outputFormatter->format( - $printer->printNamespace($namespace), - $existingContent, - ); - - return [$outputFile => $formattedContent]; - } - - /** - * @param array $nodes - * - * @return array - */ - private function discoverPrefixes(array $nodes): array - { - $prefixes = []; - - foreach ($nodes as $reflection) { - $attributes = $reflection->getAttributes(Mixin::class); - if ($attributes === []) { - continue; - } - - $mixin = $attributes[0]->newInstance(); - if ($mixin->prefix === null) { - continue; - } - - $prefixes[$mixin->prefix] = [ - 'prefix' => $mixin->prefix, - 'prefixParameter' => $mixin->prefixParameter, - ]; - } - - ksort($prefixes); - - return $prefixes; - } - - /** - * @param array $nodes - * @param array $prefixes - * - * @return array - */ - private function buildComposable(array $nodes, array $prefixes): array - { - $composable = []; - - foreach (array_keys($prefixes) as $prefix) { - $composable[$prefix] = true; - - foreach (array_keys($nodes) as $name) { - $lcName = lcfirst($name); - if ($lcName === $prefix) { - continue; - } - - if (!str_starts_with($lcName, $prefix)) { - continue; - } - - if (!ctype_upper($lcName[strlen($prefix)])) { - continue; - } - - $composable[$lcName] = true; - } - } - - uksort($composable, static fn(string $a, string $b): int => strlen($b) <=> strlen($a) ?: $a <=> $b); - - return $composable; - } - - /** - * @param array $prefixes - * - * @return array - */ - private function buildComposableWithArgument(array $prefixes): array - { - $composableWithArgument = []; - - foreach ($prefixes as $prefix => $info) { - if (!$info['prefixParameter']) { - continue; - } - - $composableWithArgument[$prefix] = true; - } - - ksort($composableWithArgument); - - return $composableWithArgument; - } -} diff --git a/src-dev/CodeGen/InterfaceConfig.php b/src-dev/CodeGen/InterfaceConfig.php deleted file mode 100644 index 92b16482d..000000000 --- a/src-dev/CodeGen/InterfaceConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -final readonly class InterfaceConfig -{ - /** - * @param array $rootExtends - * @param array $rootUses - */ - public function __construct( - public string $suffix, - public string $returnType, - public bool $static = false, - public array $rootExtends = [], - public string|null $rootComment = null, - public array $rootUses = [], - ) { - } -} diff --git a/src-dev/CodeGen/NamespaceScanner.php b/src-dev/CodeGen/NamespaceScanner.php deleted file mode 100644 index 5f9066aa4..000000000 --- a/src-dev/CodeGen/NamespaceScanner.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use DirectoryIterator; -use ReflectionClass; - -use function ksort; - -final class NamespaceScanner -{ - /** @return array */ - public static function scan(string $directory, string $namespace): array - { - $nodes = []; - - foreach (new DirectoryIterator($directory) as $file) { - if (!$file->isFile()) { - continue; - } - - $className = $namespace . '\\' . $file->getBasename('.php'); - $reflection = new ReflectionClass($className); - - if ($reflection->isAbstract()) { - continue; - } - - $nodes[$reflection->getShortName()] = $reflection; - } - - ksort($nodes); - - return $nodes; - } -} diff --git a/src-dev/CodeGen/OutputFormatter.php b/src-dev/CodeGen/OutputFormatter.php deleted file mode 100644 index 9b9066ce8..000000000 --- a/src-dev/CodeGen/OutputFormatter.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Dev\CodeGen; - -use function array_keys; -use function array_values; -use function implode; -use function preg_match; -use function preg_replace; -use function trim; - -use const PHP_EOL; - -final class OutputFormatter -{ - public function format(string $content, string $existingContent): string - { - preg_match('/^<\?php\s*\/\*[\s\S]*?\*\//', $existingContent, $matches); - $existingHeader = $matches[0] ?? ''; - - $replacements = [ - '/\n\n\t(public|private|\/\*\*)/m' => PHP_EOL . ' $1', - '/\t/m' => ' ', - '/\?([a-zA-Z]+) \$/' => '$1|null $', - '/\/\*\*\n +\* (.+)\n +\*\//m' => '/** $1 */', - ]; - - return implode(PHP_EOL, [ - trim($existingHeader) . PHP_EOL, - 'declare(strict_types=1);', - '', - preg_replace( - array_keys($replacements), - array_values($replacements), - $content, - ), - ]); - } -} diff --git a/src-dev/Commands/LintMixinCommand.php b/src-dev/Commands/LintMixinCommand.php index d9c6828de..c9fb1db2d 100644 --- a/src-dev/Commands/LintMixinCommand.php +++ b/src-dev/Commands/LintMixinCommand.php @@ -11,13 +11,14 @@ namespace Respect\Dev\Commands; -use Respect\Dev\CodeGen\Config; -use Respect\Dev\CodeGen\FluentBuilder\MethodBuilder; -use Respect\Dev\CodeGen\FluentBuilder\MixinGenerator; -use Respect\Dev\CodeGen\FluentBuilder\PrefixMapGenerator; -use Respect\Dev\CodeGen\InterfaceConfig; use Respect\Dev\Differ\ConsoleDiffer; use Respect\Dev\Differ\Item; +use Respect\FluentGen\Config; +use Respect\FluentGen\Fluent\InterfaceConfig; +use Respect\FluentGen\Fluent\MethodBuilder; +use Respect\FluentGen\Fluent\MixinGenerator; +use Respect\FluentGen\Fluent\PrefixConstantsGenerator; +use Respect\FluentGen\NamespaceScanner; use Respect\Validation\Mixins\Chain; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; @@ -67,8 +68,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int outputNamespace: 'Respect\\Validation\\Mixins', ); + $scanner = new NamespaceScanner(); + $generator = new MixinGenerator( config: $config, + scanner: $scanner, methodBuilder: new MethodBuilder( excludedTypePrefixes: ['Sokil', 'Egulias'], excludedTypeNames: ['finfo'], @@ -89,9 +93,10 @@ interfaces: [ ], ); - $prefixMapGenerator = new PrefixMapGenerator( + $prefixMapGenerator = new PrefixConstantsGenerator( config: $config, - outputClassName: 'PrefixMap', + scanner: $scanner, + outputClassName: 'PrefixConstants', ); $files = $generator->generate() + $prefixMapGenerator->generate(); diff --git a/src/ContainerRegistry.php b/src/ContainerRegistry.php index 0dcd8c536..6f6f0938b 100644 --- a/src/ContainerRegistry.php +++ b/src/ContainerRegistry.php @@ -14,6 +14,9 @@ use DI\Container; use libphonenumber\PhoneNumberUtil; use Psr\Container\ContainerInterface; +use ReflectionClass; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\FluentFactory; use Respect\StringFormatter\BypassTranslator; use Respect\StringFormatter\Modifier; use Respect\StringFormatter\Modifiers\FormatterModifier; @@ -40,8 +43,6 @@ use Respect\Validation\Message\Parameters\ResultHandler; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\TemplateRegistry; -use Respect\Validation\Transformers\Prefix; -use Respect\Validation\Transformers\Transformer; use Symfony\Contracts\Translation\TranslatorInterface; use function DI\autowire; @@ -57,7 +58,6 @@ public static function createContainer(array $definitions = []): Container { return new Container($definitions + [ PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()), - Transformer::class => create(Prefix::class), TemplateRegistry::class => create(TemplateRegistry::class), TemplateResolver::class => autowire(TemplateResolver::class), TranslatorInterface::class => autowire(BypassTranslator::class), @@ -67,11 +67,19 @@ public static function createContainer(array $definitions = []): Container 'respect.validation.formatter.full_message' => autowire(NestedListStringFormatter::class), 'respect.validation.formatter.messages' => autowire(NestedArrayFormatter::class), 'respect.validation.ignored_backtrace_paths' => [__DIR__ . '/ValidatorBuilder.php'], - 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Validators'], - ValidatorFactory::class => factory(static fn(Container $container) => new NamespacedValidatorFactory( - $container->get(Transformer::class), - $container->get('respect.validation.rule_factory.namespaces'), - )), + 'respect.validation.rule_factory.namespaces' => [], + FluentFactory::class => factory(static function (Container $container) { + $factory = (new ReflectionClass(ValidatorBuilder::class)) + ->getAttributes(FluentNamespace::class)[0] + ->newInstance() + ->factory; + + foreach ($container->get('respect.validation.rule_factory.namespaces') as $namespace) { + $factory = $factory->withNamespace($namespace); + } + + return $factory; + }), Quoter::class => create(CodeQuoter::class)->constructor(120), Handler::class => factory(static function (Container $container) { $handler = CompositeHandler::create(); @@ -101,7 +109,7 @@ public static function createContainer(array $definitions = []): Container $container->get(TranslatorInterface::class), )), ValidatorBuilder::class => factory(static fn(Container $container) => new ValidatorBuilder( - $container->get(ValidatorFactory::class), + $container->get(FluentFactory::class), $container->get(Renderer::class), $container->get('respect.validation.formatter.message'), $container->get('respect.validation.formatter.full_message'), diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index d4042eda0..431dabd04 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -182,11 +182,11 @@ public static function iterableVal(): Chain; public static function json(): Chain; - public static function key(string|int $key, Validator $validator): Chain; + public static function key(int|string $key, Validator $validator): Chain; - public static function keyExists(string|int $key): Chain; + public static function keyExists(int|string $key): Chain; - public static function keyOptional(string|int $key, Validator $validator): Chain; + public static function keyOptional(int|string $key, Validator $validator): Chain; public static function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index acc75aef5..850938790 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -184,11 +184,11 @@ public function iterableVal(): Chain; public function json(): Chain; - public function key(string|int $key, Validator $validator): Chain; + public function key(int|string $key, Validator $validator): Chain; - public function keyExists(string|int $key): Chain; + public function keyExists(int|string $key): Chain; - public function keyOptional(string|int $key, Validator $validator): Chain; + public function keyOptional(int|string $key, Validator $validator): Chain; public function keySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index 41c6c44ab..225a5e009 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -179,11 +179,11 @@ public static function notIterableVal(): Chain; public static function notJson(): Chain; - public static function notKey(string|int $key, Validator $validator): Chain; + public static function notKey(int|string $key, Validator $validator): Chain; - public static function notKeyExists(string|int $key): Chain; + public static function notKeyExists(int|string $key): Chain; - public static function notKeyOptional(string|int $key, Validator $validator): Chain; + public static function notKeyOptional(int|string $key, Validator $validator): Chain; public static function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index 113737fac..8ea5fd991 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -179,11 +179,11 @@ public function notIterableVal(): Chain; public function notJson(): Chain; - public function notKey(string|int $key, Validator $validator): Chain; + public function notKey(int|string $key, Validator $validator): Chain; - public function notKeyExists(string|int $key): Chain; + public function notKeyExists(int|string $key): Chain; - public function notKeyOptional(string|int $key, Validator $validator): Chain; + public function notKeyOptional(int|string $key, Validator $validator): Chain; public function notKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 12f54b0b2..e32cd0939 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -179,11 +179,11 @@ public static function nullOrIterableVal(): Chain; public static function nullOrJson(): Chain; - public static function nullOrKey(string|int $key, Validator $validator): Chain; + public static function nullOrKey(int|string $key, Validator $validator): Chain; - public static function nullOrKeyExists(string|int $key): Chain; + public static function nullOrKeyExists(int|string $key): Chain; - public static function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public static function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 329659911..66e87b606 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -179,11 +179,11 @@ public function nullOrIterableVal(): Chain; public function nullOrJson(): Chain; - public function nullOrKey(string|int $key, Validator $validator): Chain; + public function nullOrKey(int|string $key, Validator $validator): Chain; - public function nullOrKeyExists(string|int $key): Chain; + public function nullOrKeyExists(int|string $key): Chain; - public function nullOrKeyOptional(string|int $key, Validator $validator): Chain; + public function nullOrKeyOptional(int|string $key, Validator $validator): Chain; public function nullOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/PrefixConstants.php b/src/Mixins/PrefixConstants.php new file mode 100644 index 000000000..9afcf99e2 --- /dev/null +++ b/src/Mixins/PrefixConstants.php @@ -0,0 +1,56 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Mixins; + +final class PrefixConstants +{ + public const array COMPOSABLE = [ + 'propertyOptional' => true, + 'propertyExists' => true, + 'keyOptional' => true, + 'keyExists' => true, + 'property' => true, + 'undefOr' => true, + 'keySet' => true, + 'length' => true, + 'nullOr' => true, + 'allOf' => true, + 'all' => true, + 'key' => true, + 'max' => true, + 'min' => true, + 'not' => true, + ]; + public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; + public const array FORBIDDEN = [ + 'All' => ['all' => true], + 'Attributes' => ['all' => true, 'key' => true, 'not' => true, 'property' => true, 'undefOr' => true], + 'Blank' => ['nullOr' => true, 'undefOr' => true], + 'Exists' => ['all' => true, 'key' => true, 'property' => true], + 'Formatted' => ['all' => true, 'key' => true, 'property' => true], + 'Key' => ['all' => true, 'key' => true, 'property' => true], + 'KeyExists' => ['all' => true, 'key' => true, 'property' => true], + 'KeyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'KeySet' => ['all' => true, 'key' => true, 'property' => true], + 'Length' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Max' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Min' => ['all' => true, 'key' => true, 'length' => true, 'max' => true, 'min' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Named' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Not' => ['not' => true], + 'NullOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Property' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyExists' => ['all' => true, 'key' => true, 'property' => true], + 'PropertyOptional' => ['all' => true, 'key' => true, 'property' => true], + 'Templated' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + 'Undef' => ['nullOr' => true, 'undefOr' => true], + 'UndefOr' => ['all' => true, 'key' => true, 'not' => true, 'nullOr' => true, 'property' => true, 'undefOr' => true], + ]; +} diff --git a/src/Mixins/PrefixMap.php b/src/Mixins/PrefixMap.php deleted file mode 100644 index d27356be4..000000000 --- a/src/Mixins/PrefixMap.php +++ /dev/null @@ -1,33 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Mixins; - -final class PrefixMap -{ - public const array COMPOSABLE = [ - 'propertyOptional' => true, - 'propertyExists' => true, - 'keyOptional' => true, - 'keyExists' => true, - 'property' => true, - 'undefOr' => true, - 'keySet' => true, - 'length' => true, - 'nullOr' => true, - 'allOf' => true, - 'all' => true, - 'key' => true, - 'max' => true, - 'min' => true, - 'not' => true, - ]; - public const array COMPOSABLE_WITH_ARGUMENT = ['key' => true, 'property' => true]; -} diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index c8c6273ec..c03840e6b 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -177,11 +177,11 @@ public static function undefOrIterableVal(): Chain; public static function undefOrJson(): Chain; - public static function undefOrKey(string|int $key, Validator $validator): Chain; + public static function undefOrKey(int|string $key, Validator $validator): Chain; - public static function undefOrKeyExists(string|int $key): Chain; + public static function undefOrKeyExists(int|string $key): Chain; - public static function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public static function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public static function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index aee52fcae..7d04d2463 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -177,11 +177,11 @@ public function undefOrIterableVal(): Chain; public function undefOrJson(): Chain; - public function undefOrKey(string|int $key, Validator $validator): Chain; + public function undefOrKey(int|string $key, Validator $validator): Chain; - public function undefOrKeyExists(string|int $key): Chain; + public function undefOrKeyExists(int|string $key): Chain; - public function undefOrKeyOptional(string|int $key, Validator $validator): Chain; + public function undefOrKeyOptional(int|string $key, Validator $validator): Chain; public function undefOrKeySet(Validator $validator, Validator ...$validators): Chain; diff --git a/src/NamespacedValidatorFactory.php b/src/NamespacedValidatorFactory.php deleted file mode 100644 index 369dd8994..000000000 --- a/src/NamespacedValidatorFactory.php +++ /dev/null @@ -1,98 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -use ReflectionClass; -use ReflectionException; -use Respect\Validation\Exceptions\ComponentException; -use Respect\Validation\Exceptions\InvalidClassException; -use Respect\Validation\Transformers\Transformer; -use Respect\Validation\Transformers\ValidatorSpec; - -use function array_merge; -use function Respect\Stringifier\stringify; -use function sprintf; -use function trim; -use function ucfirst; - -final readonly class NamespacedValidatorFactory implements ValidatorFactory -{ - /** @param array $rulesNamespaces */ - public function __construct( - private Transformer $transformer, - private array $rulesNamespaces, - ) { - } - - public function withNamespace(string $rulesNamespace): self - { - return clone ($this, ['rulesNamespaces' => [trim($rulesNamespace, '\\'), ...$this->rulesNamespaces]]); - } - - /** @param array $arguments */ - public function create(string $ruleName, array $arguments = []): Validator - { - return $this->createValidatorSpec($this->transformer->transform(new ValidatorSpec($ruleName, $arguments))); - } - - private function createValidatorSpec(ValidatorSpec $validatorSpec): Validator - { - $validator = $this->createRule($validatorSpec->name, $validatorSpec->arguments); - if ($validatorSpec->wrapper !== null) { - return $this->createRule( - $validatorSpec->wrapper->name, - array_merge($validatorSpec->wrapper->arguments, [$validator]), - ); - } - - return $validator; - } - - /** @param array $arguments */ - private function createRule(string $ruleName, array $arguments = []): Validator - { - $reflection = null; - - foreach ($this->rulesNamespaces as $namespace) { - try { - /** @var class-string $name */ - $name = $namespace . '\\' . ucfirst($ruleName); - $reflection = new ReflectionClass($name); - if (!$reflection->isSubclassOf(Validator::class)) { - throw new InvalidClassException( - sprintf('"%s" must be an instance of "%s"', $name, Validator::class), - ); - } - - if (!$reflection->isInstantiable()) { - throw new InvalidClassException(sprintf('"%s" must be instantiable', $name)); - } - - break; - } catch (ReflectionException) { - continue; - } - } - - if (!$reflection) { - throw new ComponentException(sprintf('"%s" is not a valid rule name', $ruleName)); - } - - try { - return $reflection->newInstanceArgs($arguments); - } catch (ReflectionException) { - throw new InvalidClassException( - sprintf('"%s" could not be instantiated with arguments %s', $ruleName, stringify($arguments)), - ); - } - } -} diff --git a/src/Transformers/Prefix.php b/src/Transformers/Prefix.php deleted file mode 100644 index d6205bb84..000000000 --- a/src/Transformers/Prefix.php +++ /dev/null @@ -1,71 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -use Respect\Validation\Mixins\PrefixMap; - -use function array_keys; -use function array_slice; -use function implode; -use function preg_match; -use function sprintf; - -final class Prefix implements Transformer -{ - private static string|null $regex = null; - - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec - { - $matches = $this->match($validatorSpec); - if ($matches === []) { - return $validatorSpec; - } - - if (!isset(PrefixMap::COMPOSABLE_WITH_ARGUMENT[$matches['prefix']])) { - return new ValidatorSpec( - $matches['suffix'], - $validatorSpec->arguments, - new ValidatorSpec($matches['prefix']), - ); - } - - return new ValidatorSpec( - $matches['suffix'], - array_slice($validatorSpec->arguments, 1), - new ValidatorSpec($matches['prefix'], [$validatorSpec->arguments[0]]), - ); - } - - /** @return array{}|array{prefix: string, suffix: string} */ - private function match(ValidatorSpec $validatorSpec): array - { - if ($validatorSpec->wrapper !== null || isset(PrefixMap::COMPOSABLE[$validatorSpec->name])) { - return []; - } - - preg_match(self::getRegex(), $validatorSpec->name, $matches); - - if ($matches === []) { - return []; - } - - return ['prefix' => $matches['prefix'], 'suffix' => $matches['suffix']]; - } - - private static function getRegex(): string - { - return self::$regex ?? self::$regex = sprintf( - '/^(?%s)(?.+)$/', - implode('|', array_keys(PrefixMap::COMPOSABLE)), - ); - } -} diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php deleted file mode 100644 index f812487d7..000000000 --- a/src/Transformers/Transformer.php +++ /dev/null @@ -1,17 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -interface Transformer -{ - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec; -} diff --git a/src/Transformers/ValidatorSpec.php b/src/Transformers/ValidatorSpec.php deleted file mode 100644 index 33220a71c..000000000 --- a/src/Transformers/ValidatorSpec.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -final readonly class ValidatorSpec -{ - /** @param array $arguments */ - public function __construct( - public string $name, - public array $arguments = [], - public ValidatorSpec|null $wrapper = null, - ) { - } -} diff --git a/src/ValidatorBuilder.php b/src/ValidatorBuilder.php index 5c279989f..9a82dfa72 100644 --- a/src/ValidatorBuilder.php +++ b/src/ValidatorBuilder.php @@ -12,12 +12,22 @@ namespace Respect\Validation; +use Respect\Fluent\Attributes\AssuranceAssertion; +use Respect\Fluent\Attributes\AssuranceParameter; +use Respect\Fluent\Attributes\FluentNamespace; +use Respect\Fluent\Builders\Append; +use Respect\Fluent\Factories\ComposingLookup; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\FluentFactory; +use Respect\Fluent\Resolvers\ComposableMap; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\ValidationException; use Respect\Validation\Message\ArrayFormatter; use Respect\Validation\Message\Renderer; use Respect\Validation\Message\StringFormatter; use Respect\Validation\Mixins\Builder; +use Respect\Validation\Mixins\PrefixConstants; use Respect\Validation\Validators\AllOf; use Respect\Validation\Validators\Core\Nameable; use Respect\Validation\Validators\Core\ShortCircuitable; @@ -31,14 +41,15 @@ use function is_string; /** @mixin Builder */ -final readonly class ValidatorBuilder implements Nameable, ShortCircuitable +#[FluentNamespace(new ComposingLookup( + new NamespaceLookup(new Ucfirst(), Validator::class, 'Respect\\Validation\\Validators'), + new ComposableMap(PrefixConstants::COMPOSABLE, PrefixConstants::COMPOSABLE_WITH_ARGUMENT), +))] +final readonly class ValidatorBuilder extends Append implements Nameable, ShortCircuitable { - /** @var array */ - private array $validators; - /** @param array $ignoredBacktracePaths */ public function __construct( - private ValidatorFactory $validatorFactory, + FluentFactory $factory, private Renderer $renderer, private StringFormatter $mainMessageFormatter, private StringFormatter $fullMessageFormatter, @@ -47,7 +58,7 @@ public function __construct( private array $ignoredBacktracePaths, Validator ...$validators, ) { - $this->validators = $validators; + parent::__construct($factory, ...$validators); } public static function init(Validator ...$validators): self @@ -56,15 +67,17 @@ public static function init(Validator ...$validators): self return ContainerRegistry::getContainer()->get(self::class); } - return ContainerRegistry::getContainer()->get(self::class)->with(...$validators); + return ContainerRegistry::getContainer()->get(self::class)->attach(...$validators); } public function evaluate(mixed $input): Result { - $validator = match (count($this->validators)) { + $validators = $this->getValidators(); + + $validator = match (count($validators)) { 0 => throw new ComponentException('No validators have been added.'), - 1 => current($this->validators), - default => new AllOf(...$this->validators), + 1 => current($validators), + default => new AllOf(...$validators), }; return $validator->evaluate($input); @@ -72,7 +85,9 @@ public function evaluate(mixed $input): Result public function evaluateShortCircuit(mixed $input): Result { - return (new ShortCircuit(...$this->validators))->evaluate($input); + $validators = $this->getValidators(); + + return (new ShortCircuit(...$validators))->evaluate($input); } /** @param array|string|null $template */ @@ -81,38 +96,46 @@ public function validate(mixed $input, array|string|null $template = null): Resu return $this->toResultQuery($this->evaluate($input), $template); } - public function isValid(mixed $input): bool - { + #[AssuranceAssertion] + public function isValid( + #[AssuranceParameter] + mixed $input, + ): bool { return $this->evaluateShortCircuit($input)->hasPassed; } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function check(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function check( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluateShortCircuit($input), $template); } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void - { + #[AssuranceAssertion] + public function assert( + #[AssuranceParameter] + mixed $input, + array|string|Throwable|callable|null $template = null, + ): void { $this->throwOnFailure($this->evaluate($input), $template); } - public function with(Validator $validator, Validator ...$validators): self - { - return clone ($this, ['validators' => [...$this->validators, $validator, ...$validators]]); - } - /** @return array */ public function getValidators(): array { - return $this->validators; + return $this->getNodes(); } public function getName(): Name|null { - if (count($this->validators) === 1 && current($this->validators) instanceof Nameable) { - return current($this->validators)->getName(); + $validators = $this->getNodes(); + + if (count($validators) === 1 && current($validators) instanceof Nameable) { + return current($validators)->getName(); } return null; @@ -153,14 +176,8 @@ private function throwOnFailure(Result $result, array|callable|Throwable|string| } /** @param array $arguments */ - public static function __callStatic(string $ruleName, array $arguments): self + public static function __callStatic(string $ruleName, array $arguments): static { return self::init()->__call($ruleName, $arguments); } - - /** @param array $arguments */ - public function __call(string $ruleName, array $arguments): self - { - return $this->with($this->validatorFactory->create($ruleName, $arguments)); - } } diff --git a/src/ValidatorFactory.php b/src/ValidatorFactory.php deleted file mode 100644 index 24c25473e..000000000 --- a/src/ValidatorFactory.php +++ /dev/null @@ -1,18 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -interface ValidatorFactory -{ - /** @param array $arguments */ - public function create(string $ruleName, array $arguments = []): Validator; -} diff --git a/src/Validators/All.php b/src/Validators/All.php index 6a7d728b5..4593aa1e1 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,7 +15,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -23,9 +25,10 @@ use Respect\Validation\Validators\Core\FilteredArray; use Respect\Validation\Validators\Core\ShortCircuitable; -#[Mixin(prefix: 'all', exclude: ['all'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('Every item in', 'Every item in')] +#[Assurance(from: AssuranceFrom::Elements)] final class All extends FilteredArray implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/AllOf.php b/src/Validators/AllOf.php index 75701b6cc..88470c9bd 100644 --- a/src/Validators/AllOf.php +++ b/src/Validators/AllOf.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -38,6 +40,7 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] +#[Assurance(compose: AssuranceCompose::Intersect)] final class AllOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Alnum.php b/src/Validators/Alnum.php index d734082b1..7c87a508b 100644 --- a/src/Validators/Alnum.php +++ b/src/Validators/Alnum.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of letters (a-z), digits (0-9), or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Alnum extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Alpha.php b/src/Validators/Alpha.php index 986554c6a..90bbc846d 100644 --- a/src/Validators/Alpha.php +++ b/src/Validators/Alpha.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of letters (a-z) or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Alpha extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/AnyOf.php b/src/Validators/AnyOf.php index de9b8aadd..9d061b3d1 100644 --- a/src/Validators/AnyOf.php +++ b/src/Validators/AnyOf.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -30,6 +32,7 @@ '{{subject}} must pass at least one of the rules', '{{subject}} must pass at least one of the rules', )] +#[Assurance(compose: AssuranceCompose::Union)] final class AnyOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/ArrayType.php b/src/Validators/ArrayType.php index 9ada2804e..a4a9a1df0 100644 --- a/src/Validators/ArrayType.php +++ b/src/Validators/ArrayType.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be an array', '{{subject}} must not be an array', )] +#[Assurance(type: 'array')] final class ArrayType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/ArrayVal.php b/src/Validators/ArrayVal.php index cede9834c..f12267328 100644 --- a/src/Validators/ArrayVal.php +++ b/src/Validators/ArrayVal.php @@ -18,6 +18,7 @@ use ArrayAccess; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SimpleXMLElement; @@ -29,6 +30,7 @@ '{{subject}} must be an array', '{{subject}} must not be an array', )] +#[Assurance(type: ['array', ArrayAccess::class, SimpleXMLElement::class])] final class ArrayVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Attributes.php b/src/Validators/Attributes.php index 78ff90583..13210b052 100644 --- a/src/Validators/Attributes.php +++ b/src/Validators/Attributes.php @@ -17,13 +17,13 @@ use ReflectionClass; use ReflectionObject; use ReflectionProperty; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Id; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Reducer; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class Attributes implements Validator { diff --git a/src/Validators/Base64.php b/src/Validators/Base64.php index 560857a02..21cf3543e 100644 --- a/src/Validators/Base64.php +++ b/src/Validators/Base64.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a base64-encoded string', '{{subject}} must not be a base64-encoded string', )] +#[Assurance(type: 'string')] final class Base64 extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Between.php b/src/Validators/Between.php index 7ef768e25..862dd6a86 100644 --- a/src/Validators/Between.php +++ b/src/Validators/Between.php @@ -15,13 +15,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be between {{minValue}} and {{maxValue}}', diff --git a/src/Validators/BetweenExclusive.php b/src/Validators/BetweenExclusive.php index 41ee52218..3fd65054c 100644 --- a/src/Validators/BetweenExclusive.php +++ b/src/Validators/BetweenExclusive.php @@ -12,13 +12,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanCompareValues; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{minValue}} and less than {{maxValue}}', diff --git a/src/Validators/Blank.php b/src/Validators/Blank.php index 067864ce6..c83737ba6 100644 --- a/src/Validators/Blank.php +++ b/src/Validators/Blank.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -27,7 +27,7 @@ use function is_string; use function trim; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be blank', diff --git a/src/Validators/BoolType.php b/src/Validators/BoolType.php index 49626f90b..4e7c1e1af 100644 --- a/src/Validators/BoolType.php +++ b/src/Validators/BoolType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a boolean', '{{subject}} must not be a boolean', )] +#[Assurance(type: 'bool')] final class BoolType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/BoolVal.php b/src/Validators/BoolVal.php index 277ca2409..24b4b06a7 100644 --- a/src/Validators/BoolVal.php +++ b/src/Validators/BoolVal.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be a boolean', '{{subject}} must not be a boolean', )] +#[Assurance(type: 'bool')] final class BoolVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Bsn.php b/src/Validators/Bsn.php index 7d4ac1838..b357c0979 100644 --- a/src/Validators/Bsn.php +++ b/src/Validators/Bsn.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a BSN', '{{subject}} must not be a BSN', )] +#[Assurance(type: 'string')] final class Bsn extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CallableType.php b/src/Validators/CallableType.php index 04dc72cb0..3a543eaf7 100644 --- a/src/Validators/CallableType.php +++ b/src/Validators/CallableType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a callable function', '{{subject}} must not be a callable function', )] +#[Assurance(type: 'callable')] final class CallableType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Charset.php b/src/Validators/Charset.php index 7ac4c01d3..a4cbb77b4 100644 --- a/src/Validators/Charset.php +++ b/src/Validators/Charset.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -33,6 +34,7 @@ '{{subject}} must consist only of characters from the {{charset|list:or}} character-set', '{{subject}} must not consist only of characters from the {{charset|list:or}} character-set', )] +#[Assurance(type: 'string')] final readonly class Charset implements Validator { /** @var non-empty-array */ diff --git a/src/Validators/Cnh.php b/src/Validators/Cnh.php index 50b9a5664..eef2cd35b 100644 --- a/src/Validators/Cnh.php +++ b/src/Validators/Cnh.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -30,6 +31,7 @@ '{{subject}} must be a CNH', '{{subject}} must not be a CNH', )] +#[Assurance(type: 'string')] final class Cnh extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Cnpj.php b/src/Validators/Cnpj.php index 3cfc56918..d226baee8 100644 --- a/src/Validators/Cnpj.php +++ b/src/Validators/Cnpj.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -34,6 +35,7 @@ '{{subject}} must be a CNPJ', '{{subject}} must not be a CNPJ', )] +#[Assurance(type: 'string')] final class Cnpj extends Simple { private const int BASE_ASCII = 48; diff --git a/src/Validators/Consonant.php b/src/Validators/Consonant.php index da517afb5..fd5d2be35 100644 --- a/src/Validators/Consonant.php +++ b/src/Validators/Consonant.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -33,6 +34,7 @@ '{{subject}} must not consist only of consonants or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Consonant extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Control.php b/src/Validators/Control.php index dc6dea0db..da9c0f618 100644 --- a/src/Validators/Control.php +++ b/src/Validators/Control.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of control characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Control extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Countable.php b/src/Validators/Countable.php index 7b75be4f0..e61d0cf28 100644 --- a/src/Validators/Countable.php +++ b/src/Validators/Countable.php @@ -20,6 +20,7 @@ use Attribute; use Countable as CountableInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -30,6 +31,7 @@ '{{subject}} must be countable', '{{subject}} must not be countable', )] +#[Assurance(type: ['array', CountableInterface::class])] final class Countable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CountryCode.php b/src/Validators/CountryCode.php index 93894f565..6a53f68cf 100644 --- a/src/Validators/CountryCode.php +++ b/src/Validators/CountryCode.php @@ -19,6 +19,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -35,6 +36,7 @@ '{{subject}} must be a country code', '{{subject}} must not be a country code', )] +#[Assurance(type: 'string')] final readonly class CountryCode implements Validator { private Countries $countries; diff --git a/src/Validators/Cpf.php b/src/Validators/Cpf.php index 1e2f69f81..2fd72c869 100644 --- a/src/Validators/Cpf.php +++ b/src/Validators/Cpf.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -33,6 +34,7 @@ '{{subject}} must be a CPF', '{{subject}} must not be a CPF', )] +#[Assurance(type: 'string')] final class Cpf extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/CreditCard.php b/src/Validators/CreditCard.php index 23ed28857..fd429fe02 100644 --- a/src/Validators/CreditCard.php +++ b/src/Validators/CreditCard.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -41,6 +42,7 @@ '{{subject}} must not be a {{brand|raw}} credit card number', self::TEMPLATE_BRANDED, )] +#[Assurance(type: 'string')] final readonly class CreditCard implements Validator { public const string TEMPLATE_BRANDED = '__branded__'; diff --git a/src/Validators/CurrencyCode.php b/src/Validators/CurrencyCode.php index 74af78c08..f4314e98e 100644 --- a/src/Validators/CurrencyCode.php +++ b/src/Validators/CurrencyCode.php @@ -16,6 +16,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -31,6 +32,7 @@ '{{subject}} must be a currency code', '{{subject}} must not be a currency code', )] +#[Assurance(type: 'string')] final readonly class CurrencyCode implements Validator { private Currencies $currencies; diff --git a/src/Validators/Date.php b/src/Validators/Date.php index 151ee9b0a..d9f391e56 100644 --- a/src/Validators/Date.php +++ b/src/Validators/Date.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -33,6 +34,7 @@ '{{subject}} must be a date in the {{sample}} format', '{{subject}} must not be a date in the {{sample}} format', )] +#[Assurance(type: 'string')] final readonly class Date implements Validator { use CanValidateDateTime; diff --git a/src/Validators/DateTime.php b/src/Validators/DateTime.php index 8cef8308e..f9ef97590 100644 --- a/src/Validators/DateTime.php +++ b/src/Validators/DateTime.php @@ -18,6 +18,7 @@ use Attribute; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -38,6 +39,7 @@ '{{subject}} must not be a date/time in the {{sample}} format', self::TEMPLATE_FORMAT, )] +#[Assurance(type: ['string', DateTimeInterface::class])] final class DateTime implements Validator { use CanValidateDateTime; diff --git a/src/Validators/DateTimeDiff.php b/src/Validators/DateTimeDiff.php index 194a117d1..96373a362 100644 --- a/src/Validators/DateTimeDiff.php +++ b/src/Validators/DateTimeDiff.php @@ -14,6 +14,7 @@ use Attribute; use DateTimeImmutable; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -44,6 +45,7 @@ 'For comparison with {{now|raw}}, {{subject}} must not be a datetime in the format {{sample|raw}}', self::TEMPLATE_WRONG_FORMAT, )] +#[Assurance(type: ['string', DateTimeInterface::class])] final readonly class DateTimeDiff implements Validator { use CanValidateDateTime; diff --git a/src/Validators/Decimal.php b/src/Validators/Decimal.php index 424109587..238275b4b 100644 --- a/src/Validators/Decimal.php +++ b/src/Validators/Decimal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must have {{decimals}} decimal places', '{{subject}} must not have {{decimals}} decimal places', )] +#[Assurance(type: 'string')] final readonly class Decimal implements Validator { public function __construct( diff --git a/src/Validators/Digit.php b/src/Validators/Digit.php index 5d432749b..213cbcf04 100644 --- a/src/Validators/Digit.php +++ b/src/Validators/Digit.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of digits (0-9) or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Digit extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Directory.php b/src/Validators/Directory.php index 4437ac4f0..f3987dca3 100644 --- a/src/Validators/Directory.php +++ b/src/Validators/Directory.php @@ -16,6 +16,7 @@ use Attribute; use Directory as NativeDirectory; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -28,6 +29,7 @@ '{{subject}} must be an accessible existing directory', '{{subject}} must not be an accessible existing directory', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class Directory extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Domain.php b/src/Validators/Domain.php index e4ec03bc0..5053fe80a 100644 --- a/src/Validators/Domain.php +++ b/src/Validators/Domain.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -30,6 +31,7 @@ '{{subject}} must be an internet domain', '{{subject}} must not be an internet domain', )] +#[Assurance(type: 'string')] final class Domain implements Validator { private readonly Validator $genericRule; diff --git a/src/Validators/Each.php b/src/Validators/Each.php index 43d24f04f..c3aa48ebd 100644 --- a/src/Validators/Each.php +++ b/src/Validators/Each.php @@ -18,6 +18,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; @@ -32,6 +34,7 @@ 'Each item in {{subject}} must be valid', 'Each item in {{subject}} must be invalid', )] +#[Assurance(from: AssuranceFrom::Elements)] final class Each extends FilteredArray implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Email.php b/src/Validators/Email.php index 5877204e5..bed560404 100644 --- a/src/Validators/Email.php +++ b/src/Validators/Email.php @@ -21,6 +21,7 @@ use Attribute; use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -36,6 +37,7 @@ '{{subject}} must be an email address', '{{subject}} must not be an email address', )] +#[Assurance(type: 'string')] final class Email extends Simple { private readonly EmailValidator|null $validator; diff --git a/src/Validators/Emoji.php b/src/Validators/Emoji.php index fa258c4c1..141d084bb 100644 --- a/src/Validators/Emoji.php +++ b/src/Validators/Emoji.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -23,6 +24,7 @@ '{{subject}} must be an emoji', '{{subject}} must not be an emoji', )] +#[Assurance(type: 'string')] final class Emoji extends Simple { private const string REGEX = <<<'REGEX' diff --git a/src/Validators/Equals.php b/src/Validators/Equals.php index d998f5df0..04ab4932a 100644 --- a/src/Validators/Equals.php +++ b/src/Validators/Equals.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function is_scalar; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equal to {{compareTo}}', diff --git a/src/Validators/Equivalent.php b/src/Validators/Equivalent.php index fc3cf6247..a9c41a8b7 100644 --- a/src/Validators/Equivalent.php +++ b/src/Validators/Equivalent.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; use function is_scalar; use function mb_strtoupper; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be equivalent to {{compareTo}}', diff --git a/src/Validators/Even.php b/src/Validators/Even.php index 5208abf90..4fea0f782 100644 --- a/src/Validators/Even.php +++ b/src/Validators/Even.php @@ -18,7 +18,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,12 +27,13 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an even number', '{{subject}} must be an odd number', )] +#[Assurance(type: 'int')] final class Even extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Executable.php b/src/Validators/Executable.php index 6e056f740..b1e4b965b 100644 --- a/src/Validators/Executable.php +++ b/src/Validators/Executable.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing executable file', '{{subject}} must not be an accessible existing executable file', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class Executable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Exists.php b/src/Validators/Exists.php index dee5bab98..3d7ce0def 100644 --- a/src/Validators/Exists.php +++ b/src/Validators/Exists.php @@ -15,7 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -23,12 +24,13 @@ use function file_exists; use function is_string; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an existing file', '{{subject}} must not be an existing file', )] +#[Assurance(type: 'string')] final class Exists extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Extension.php b/src/Validators/Extension.php index 317fef0c6..7d4e7b742 100644 --- a/src/Validators/Extension.php +++ b/src/Validators/Extension.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must have the {{extension}} extension', '{{subject}} must not have the {{extension}} extension', )] +#[Assurance(type: ['string', SplFileInfo::class])] final readonly class Extension implements Validator { public function __construct( diff --git a/src/Validators/Factor.php b/src/Validators/Factor.php index bfdcdcacc..c1a501410 100644 --- a/src/Validators/Factor.php +++ b/src/Validators/Factor.php @@ -14,7 +14,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,12 +25,13 @@ use function is_numeric; use function preg_match; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a factor of {{dividend|raw}}', '{{subject}} must not be a factor of {{dividend|raw}}', )] +#[Assurance(type: 'int')] final readonly class Factor implements Validator { public function __construct( diff --git a/src/Validators/FalseVal.php b/src/Validators/FalseVal.php index e5f1fc76a..c09599b6a 100644 --- a/src/Validators/FalseVal.php +++ b/src/Validators/FalseVal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must evaluate to `false`', '{{subject}} must not evaluate to `false`', )] +#[Assurance(type: 'false')] final class FalseVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/File.php b/src/Validators/File.php index 2d48d3d55..e0625d7d7 100644 --- a/src/Validators/File.php +++ b/src/Validators/File.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing file', '{{subject}} must not be an accessible existing file', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class File extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Finite.php b/src/Validators/Finite.php index 9c917861b..dcd835a9e 100644 --- a/src/Validators/Finite.php +++ b/src/Validators/Finite.php @@ -15,19 +15,21 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_finite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a finite number', '{{subject}} must not be a finite number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Finite extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/FloatType.php b/src/Validators/FloatType.php index a94de207c..f87feb076 100644 --- a/src/Validators/FloatType.php +++ b/src/Validators/FloatType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a float', '{{subject}} must not be a float', )] +#[Assurance(type: 'float')] final class FloatType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/FloatVal.php b/src/Validators/FloatVal.php index 515327314..3f034baca 100644 --- a/src/Validators/FloatVal.php +++ b/src/Validators/FloatVal.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a floating-point number', '{{subject}} must not be a floating-point number', )] +#[Assurance(type: 'int|float|numeric-string')] final class FloatVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Format.php b/src/Validators/Format.php index 99a35a975..c56e19332 100644 --- a/src/Validators/Format.php +++ b/src/Validators/Format.php @@ -12,16 +12,19 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\StringFormatter\Formatter; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; +use Stringable; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be formatted as {{formatted}}', '{{subject}} must not be formatted as {{formatted}}', )] +#[Assurance(type: ['scalar', Stringable::class])] final readonly class Format implements Validator { public function __construct( diff --git a/src/Validators/Formatted.php b/src/Validators/Formatted.php index 23696da5c..eda66f220 100644 --- a/src/Validators/Formatted.php +++ b/src/Validators/Formatted.php @@ -11,12 +11,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\StringFormatter\Formatter; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Formatted implements Validator { diff --git a/src/Validators/Graph.php b/src/Validators/Graph.php index 4c0c40de5..e36bed049 100644 --- a/src/Validators/Graph.php +++ b/src/Validators/Graph.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of printable non-spacing characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Graph extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/GreaterThan.php b/src/Validators/GreaterThan.php index cafa657b5..af6f5cf9a 100644 --- a/src/Validators/GreaterThan.php +++ b/src/Validators/GreaterThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than {{compareTo}}', diff --git a/src/Validators/GreaterThanOrEqual.php b/src/Validators/GreaterThanOrEqual.php index 0d273fc23..41a22a2ac 100644 --- a/src/Validators/GreaterThanOrEqual.php +++ b/src/Validators/GreaterThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be greater than or equal to {{compareTo}}', diff --git a/src/Validators/Hetu.php b/src/Validators/Hetu.php index c3025f403..c9203f164 100644 --- a/src/Validators/Hetu.php +++ b/src/Validators/Hetu.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a Finnish personal identity code', '{{subject}} must not be a Finnish personal identity code', )] +#[Assurance(type: 'string')] final class Hetu extends Simple { use CanValidateDateTime; diff --git a/src/Validators/HexRgbColor.php b/src/Validators/HexRgbColor.php index 5203ee073..c1b85b8ff 100644 --- a/src/Validators/HexRgbColor.php +++ b/src/Validators/HexRgbColor.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -21,6 +22,7 @@ '{{subject}} must be a hex RGB color', '{{subject}} must not be a hex RGB color', )] +#[Assurance(type: 'string')] final class HexRgbColor extends Envelope { public function __construct() diff --git a/src/Validators/Iban.php b/src/Validators/Iban.php index 5f0d5eeec..b28fbedb9 100644 --- a/src/Validators/Iban.php +++ b/src/Validators/Iban.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be an IBAN', '{{subject}} must not be an IBAN', )] +#[Assurance(type: 'string')] final class Iban extends Simple { private const array COUNTRIES_LENGTHS = [ diff --git a/src/Validators/Identical.php b/src/Validators/Identical.php index 25380531e..ebfb1e444 100644 --- a/src/Validators/Identical.php +++ b/src/Validators/Identical.php @@ -15,17 +15,20 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be identical to {{compareTo}}', '{{subject}} must not be identical to {{compareTo}}', )] +#[Assurance(from: AssuranceFrom::Value)] final readonly class Identical implements Validator { public function __construct( diff --git a/src/Validators/Image.php b/src/Validators/Image.php index da81a7da0..c5f24014f 100644 --- a/src/Validators/Image.php +++ b/src/Validators/Image.php @@ -15,6 +15,7 @@ use Attribute; use finfo; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -31,6 +32,7 @@ '{{subject}} must be an accessible existing image file', '{{subject}} must not be an accessible existing image file', )] +#[Assurance(type: 'string')] final readonly class Image implements Validator { public function evaluate(mixed $input): Result diff --git a/src/Validators/Imei.php b/src/Validators/Imei.php index 9f8b4ae6a..487eee327 100644 --- a/src/Validators/Imei.php +++ b/src/Validators/Imei.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be an IMEI number', '{{subject}} must not be an IMEI number', )] +#[Assurance(type: 'string')] final class Imei extends Simple { private const int IMEI_SIZE = 15; diff --git a/src/Validators/In.php b/src/Validators/In.php index 53d46d9b7..d6870e162 100644 --- a/src/Validators/In.php +++ b/src/Validators/In.php @@ -16,7 +16,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceFrom; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -25,12 +27,13 @@ use function is_array; use function mb_strpos; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be in {{haystack}}', '{{subject}} must not be in {{haystack}}', )] +#[Assurance(from: AssuranceFrom::Member)] final readonly class In implements Validator { public function __construct( diff --git a/src/Validators/Infinite.php b/src/Validators/Infinite.php index 34bcc67cd..eaff0a239 100644 --- a/src/Validators/Infinite.php +++ b/src/Validators/Infinite.php @@ -15,19 +15,21 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_infinite; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an infinite number', '{{subject}} must not be an infinite number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Infinite extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Instance.php b/src/Validators/Instance.php index 5c63858fa..a924198e3 100644 --- a/src/Validators/Instance.php +++ b/src/Validators/Instance.php @@ -15,6 +15,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceParameter; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -24,10 +26,12 @@ '{{subject}} must be an instance of {{class|quote}}', '{{subject}} must not be an instance of {{class|quote}}', )] +#[Assurance] final readonly class Instance implements Validator { /** @param class-string $class */ public function __construct( + #[AssuranceParameter] private string $class, ) { } diff --git a/src/Validators/IntType.php b/src/Validators/IntType.php index 85ad69e57..16ccf8461 100644 --- a/src/Validators/IntType.php +++ b/src/Validators/IntType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an integer', '{{subject}} must not be an integer', )] +#[Assurance(type: 'int')] final class IntType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/IntVal.php b/src/Validators/IntVal.php index d20783cc8..94b52b226 100644 --- a/src/Validators/IntVal.php +++ b/src/Validators/IntVal.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be an integer', '{{subject}} must not be an integer', )] +#[Assurance(type: 'int|numeric-string')] final class IntVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Ip.php b/src/Validators/Ip.php index f4c26e2b9..3c0971778 100644 --- a/src/Validators/Ip.php +++ b/src/Validators/Ip.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -49,6 +50,7 @@ '{{subject}} must not be an IP address in the {{range|raw}} range', self::TEMPLATE_NETWORK_RANGE, )] +#[Assurance(type: 'string')] final class Ip implements Validator { public const string TEMPLATE_NETWORK_RANGE = '__network_range__'; diff --git a/src/Validators/Isbn.php b/src/Validators/Isbn.php index a9c8716be..eb147a0ac 100644 --- a/src/Validators/Isbn.php +++ b/src/Validators/Isbn.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must be an ISBN', '{{subject}} must not be an ISBN', )] +#[Assurance(type: 'string')] final class Isbn extends Simple { /** @see https://howtodoinjava.com/regex/java-regex-validate-international-standard-book-number-isbns */ diff --git a/src/Validators/IterableType.php b/src/Validators/IterableType.php index 249b98359..d6c45650d 100644 --- a/src/Validators/IterableType.php +++ b/src/Validators/IterableType.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be iterable', '{{subject}} must not iterable', )] +#[Assurance(type: 'iterable')] final class IterableType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/IterableVal.php b/src/Validators/IterableVal.php index a8b39f7b3..d7b80428e 100644 --- a/src/Validators/IterableVal.php +++ b/src/Validators/IterableVal.php @@ -16,15 +16,19 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\CanValidateIterable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; +use stdClass; +use Traversable; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be iterable', '{{subject}} must not be iterable', )] +#[Assurance(type: ['array', stdClass::class, Traversable::class])] final class IterableVal extends Simple { use CanValidateIterable; diff --git a/src/Validators/Json.php b/src/Validators/Json.php index 643f80052..9e9eea5f5 100644 --- a/src/Validators/Json.php +++ b/src/Validators/Json.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a JSON string', '{{subject}} must not be a JSON string', )] +#[Assurance(type: 'string')] final class Json extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Key.php b/src/Validators/Key.php index f7ac017ce..0308d1b5d 100644 --- a/src/Validators/Key.php +++ b/src/Validators/Key.php @@ -16,17 +16,19 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(prefix: 'key', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, self::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Key implements KeyRelated { public function __construct( + #[ComposableParameter] private int|string $key, private Validator $validator, ) { diff --git a/src/Validators/KeyExists.php b/src/Validators/KeyExists.php index c8e0a73aa..f7bf8462b 100644 --- a/src/Validators/KeyExists.php +++ b/src/Validators/KeyExists.php @@ -13,7 +13,7 @@ use ArrayAccess; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -23,7 +23,7 @@ use function array_key_exists; use function is_array; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/KeyOptional.php b/src/Validators/KeyOptional.php index 3bd94596b..d802151e2 100644 --- a/src/Validators/KeyOptional.php +++ b/src/Validators/KeyOptional.php @@ -13,12 +13,12 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\KeyRelated; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class KeyOptional implements KeyRelated { diff --git a/src/Validators/KeySet.php b/src/Validators/KeySet.php index 499a66238..5f878d044 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -15,7 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; @@ -33,7 +33,7 @@ use function array_merge; use function array_slice; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} validation failed', diff --git a/src/Validators/LanguageCode.php b/src/Validators/LanguageCode.php index 0199c1ae9..ed28ea9ce 100644 --- a/src/Validators/LanguageCode.php +++ b/src/Validators/LanguageCode.php @@ -15,6 +15,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -31,6 +32,7 @@ '{{subject}} must be a language code', '{{subject}} must not be a language code', )] +#[Assurance(type: 'string')] final readonly class LanguageCode implements Validator { private Languages $languages; diff --git a/src/Validators/LeapDate.php b/src/Validators/LeapDate.php index 08c608826..abd433e80 100644 --- a/src/Validators/LeapDate.php +++ b/src/Validators/LeapDate.php @@ -18,6 +18,7 @@ use Attribute; use DateTimeImmutable; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a leap date', '{{subject}} must not be a leap date', )] +#[Assurance(type: 'string')] final class LeapDate extends Simple { public function __construct( diff --git a/src/Validators/LeapYear.php b/src/Validators/LeapYear.php index b0fafefaf..165ca58b2 100644 --- a/src/Validators/LeapYear.php +++ b/src/Validators/LeapYear.php @@ -17,6 +17,7 @@ use Attribute; use DateTimeInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a leap year', '{{subject}} must not be a leap year', )] +#[Assurance(type: 'int|string')] final class LeapYear extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Length.php b/src/Validators/Length.php index feafa0ee6..5f7eead39 100644 --- a/src/Validators/Length.php +++ b/src/Validators/Length.php @@ -20,7 +20,7 @@ use Attribute; use Countable as PhpCountable; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -30,7 +30,7 @@ use function is_string; use function mb_strlen; -#[Mixin(prefix: 'length', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'The length of', diff --git a/src/Validators/LessThan.php b/src/Validators/LessThan.php index eb2039b69..715d98337 100644 --- a/src/Validators/LessThan.php +++ b/src/Validators/LessThan.php @@ -15,11 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than {{compareTo}}', diff --git a/src/Validators/LessThanOrEqual.php b/src/Validators/LessThanOrEqual.php index 7734befb5..28e22730a 100644 --- a/src/Validators/LessThanOrEqual.php +++ b/src/Validators/LessThanOrEqual.php @@ -14,11 +14,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Comparison; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be less than or equal to {{compareTo}}', diff --git a/src/Validators/Lowercase.php b/src/Validators/Lowercase.php index dddd38ce3..d7031879d 100644 --- a/src/Validators/Lowercase.php +++ b/src/Validators/Lowercase.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must consist only of lowercase letters', '{{subject}} must not consist only of lowercase letters', )] +#[Assurance(type: 'string')] final class Lowercase extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Luhn.php b/src/Validators/Luhn.php index f77197f80..2e8f074d0 100644 --- a/src/Validators/Luhn.php +++ b/src/Validators/Luhn.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a Luhn number', '{{subject}} must not be a Luhn number', )] +#[Assurance(type: 'string')] final class Luhn extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/MacAddress.php b/src/Validators/MacAddress.php index 158d07858..40f610005 100644 --- a/src/Validators/MacAddress.php +++ b/src/Validators/MacAddress.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a MAC address', '{{subject}} must not be a MAC address', )] +#[Assurance(type: 'string')] final class MacAddress extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Max.php b/src/Validators/Max.php index 9d89607bb..d31f30374 100644 --- a/src/Validators/Max.php +++ b/src/Validators/Max.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function max; -#[Mixin(prefix: 'max', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The maximum of', 'The maximum of')] final class Max extends FilteredArray diff --git a/src/Validators/Mimetype.php b/src/Validators/Mimetype.php index affef309c..45fd4a753 100644 --- a/src/Validators/Mimetype.php +++ b/src/Validators/Mimetype.php @@ -14,6 +14,7 @@ use Attribute; use finfo; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -29,6 +30,7 @@ '{{subject}} must have the {{mimetype}} MIME type', '{{subject}} must not have the {{mimetype}} MIME type', )] +#[Assurance(type: 'string')] final readonly class Mimetype implements Validator { public function __construct( diff --git a/src/Validators/Min.php b/src/Validators/Min.php index 9083b43c8..6d1812cbf 100644 --- a/src/Validators/Min.php +++ b/src/Validators/Min.php @@ -15,14 +15,14 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; use function min; -#[Mixin(prefix: 'min', requireInclusion: true)] +#[Composable(prefix: self::class, optIn: true)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('The minimum of', 'The minimum of')] final class Min extends FilteredArray diff --git a/src/Validators/Multiple.php b/src/Validators/Multiple.php index e780ff802..afbddd97a 100644 --- a/src/Validators/Multiple.php +++ b/src/Validators/Multiple.php @@ -17,17 +17,19 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a multiple of {{multipleOf}}', '{{subject}} must not be a multiple of {{multipleOf}}', )] +#[Assurance(type: 'int')] final readonly class Multiple implements Validator { public function __construct( diff --git a/src/Validators/Named.php b/src/Validators/Named.php index db58b9dc4..7da7b0a5c 100644 --- a/src/Validators/Named.php +++ b/src/Validators/Named.php @@ -12,7 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Name; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -20,7 +20,7 @@ use function is_string; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Named implements Nameable { diff --git a/src/Validators/Negative.php b/src/Validators/Negative.php index 6eebb5ee4..f03ea6ae5 100644 --- a/src/Validators/Negative.php +++ b/src/Validators/Negative.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a negative number', '{{subject}} must not be a negative number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Negative extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NfeAccessKey.php b/src/Validators/NfeAccessKey.php index 33dcc0139..d99d5c38e 100644 --- a/src/Validators/NfeAccessKey.php +++ b/src/Validators/NfeAccessKey.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must be a NFe access key', '{{subject}} must not be a NFe access key', )] +#[Assurance(type: 'string')] final class NfeAccessKey extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Nif.php b/src/Validators/Nif.php index ef01ae7b8..169b7f76d 100644 --- a/src/Validators/Nif.php +++ b/src/Validators/Nif.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a NIF', '{{subject}} must not be a NIF', )] +#[Assurance(type: 'string')] final class Nif extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Nip.php b/src/Validators/Nip.php index fa015a0a0..253afc2c7 100644 --- a/src/Validators/Nip.php +++ b/src/Validators/Nip.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a Polish VAT identification number', '{{subject}} must not be a Polish VAT identification number', )] +#[Assurance(type: 'string')] final class Nip extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NoneOf.php b/src/Validators/NoneOf.php index e0838ce87..1f309db98 100644 --- a/src/Validators/NoneOf.php +++ b/src/Validators/NoneOf.php @@ -15,6 +15,9 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; +use Respect\Fluent\Attributes\AssuranceModifier; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -34,6 +37,7 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] +#[Assurance(compose: AssuranceCompose::Union, modifier: AssuranceModifier::Exclude)] final class NoneOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Not.php b/src/Validators/Not.php index 1026bb6c6..2363fce30 100644 --- a/src/Validators/Not.php +++ b/src/Validators/Not.php @@ -18,12 +18,15 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'not', exclude: ['not'])] +#[Composable(prefix: self::class, without: [self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +#[Assurance(modifier: AssuranceModifier::Exclude)] final readonly class Not implements Validator { public function __construct( diff --git a/src/Validators/NullOr.php b/src/Validators/NullOr.php index 9b02f1500..a026090ba 100644 --- a/src/Validators/NullOr.php +++ b/src/Validators/NullOr.php @@ -14,19 +14,25 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use function array_map; -#[Mixin(prefix: 'nullOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, self::class, UndefOr::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be null', 'and must not be null', )] +#[Assurance(modifier: AssuranceModifier::Nullable)] final readonly class NullOr implements Validator { public function __construct( diff --git a/src/Validators/NullType.php b/src/Validators/NullType.php index d6c15df8c..da29bd2ee 100644 --- a/src/Validators/NullType.php +++ b/src/Validators/NullType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -22,6 +23,7 @@ '{{subject}} must be null', '{{subject}} must not be null', )] +#[Assurance(type: 'null')] final class NullType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Number.php b/src/Validators/Number.php index eac0169fa..220a88c70 100644 --- a/src/Validators/Number.php +++ b/src/Validators/Number.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -28,6 +29,7 @@ '{{subject}} must be a number', '{{subject}} must not be a number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Number extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/NumericVal.php b/src/Validators/NumericVal.php index 5a5f27253..94d103981 100644 --- a/src/Validators/NumericVal.php +++ b/src/Validators/NumericVal.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be numeric', '{{subject}} must not be numeric', )] +#[Assurance(type: 'int|float|numeric-string')] final class NumericVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/ObjectType.php b/src/Validators/ObjectType.php index 5c9dfccaa..5ac989ac6 100644 --- a/src/Validators/ObjectType.php +++ b/src/Validators/ObjectType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an object', '{{subject}} must not be an object', )] +#[Assurance(type: 'object')] final class ObjectType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Odd.php b/src/Validators/Odd.php index bc0331fd7..ec252b8c9 100644 --- a/src/Validators/Odd.php +++ b/src/Validators/Odd.php @@ -17,7 +17,8 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,12 +27,13 @@ use const FILTER_VALIDATE_INT; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be an odd number', '{{subject}} must be an even number', )] +#[Assurance(type: 'int')] final class Odd extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/OneOf.php b/src/Validators/OneOf.php index 11c871130..1d42c6196 100644 --- a/src/Validators/OneOf.php +++ b/src/Validators/OneOf.php @@ -16,6 +16,8 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -40,6 +42,7 @@ '{{subject}} must pass only one of the rules', self::TEMPLATE_MORE_THAN_ONE, )] +#[Assurance(compose: AssuranceCompose::Union)] final class OneOf extends LogicalComposite implements ShortCircuitable { use CanEvaluateShortCircuit; diff --git a/src/Validators/Pesel.php b/src/Validators/Pesel.php index 67f2421bc..936f6f6f4 100644 --- a/src/Validators/Pesel.php +++ b/src/Validators/Pesel.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a PESEL', '{{subject}} must not be a PESEL', )] +#[Assurance(type: 'string')] final class Pesel extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Phone.php b/src/Validators/Phone.php index 78f8f3881..95452d907 100644 --- a/src/Validators/Phone.php +++ b/src/Validators/Phone.php @@ -22,6 +22,7 @@ use libphonenumber\NumberParseException; use libphonenumber\PhoneNumberUtil; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -43,6 +44,7 @@ '{{subject}} must not be a phone number for country {{countryName|trans}}', self::TEMPLATE_FOR_COUNTRY, )] +#[Assurance(type: 'string')] final class Phone implements Validator { public const string TEMPLATE_FOR_COUNTRY = '__for_country__'; diff --git a/src/Validators/Pis.php b/src/Validators/Pis.php index 9ddfbdbf2..7e3811577 100644 --- a/src/Validators/Pis.php +++ b/src/Validators/Pis.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a PIS number', '{{subject}} must not be a PIS number', )] +#[Assurance(type: 'string')] final class Pis extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/PolishIdCard.php b/src/Validators/PolishIdCard.php index 602a1267a..636204a14 100644 --- a/src/Validators/PolishIdCard.php +++ b/src/Validators/PolishIdCard.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -25,6 +26,7 @@ '{{subject}} must be a Polish Identity Card number', '{{subject}} must not be a Polish Identity Card number', )] +#[Assurance(type: 'string')] final class PolishIdCard extends Simple { private const int ASCII_CODE_0 = 48; diff --git a/src/Validators/PortugueseNif.php b/src/Validators/PortugueseNif.php index c163d8d0a..9b4fb7aca 100644 --- a/src/Validators/PortugueseNif.php +++ b/src/Validators/PortugueseNif.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -32,6 +33,7 @@ '{{subject}} must be a Portuguese NIF', '{{subject}} must not be a Portuguese NIF', )] +#[Assurance(type: 'string')] final class PortugueseNif extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Positive.php b/src/Validators/Positive.php index 748180d63..9607a69b1 100644 --- a/src/Validators/Positive.php +++ b/src/Validators/Positive.php @@ -15,18 +15,20 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use function is_numeric; -#[Mixin(include: ['length', 'max', 'min'])] +#[Composable(with: [Length::class, Max::class, Min::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a positive number', '{{subject}} must not be a positive number', )] +#[Assurance(type: 'int|float|numeric-string')] final class Positive extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/PostalCode.php b/src/Validators/PostalCode.php index 9c74df8ab..508982b25 100644 --- a/src/Validators/PostalCode.php +++ b/src/Validators/PostalCode.php @@ -26,6 +26,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; @@ -37,6 +38,7 @@ '{{subject}} must be a postal code for {{countryCode}}', '{{subject}} must not be a postal code for {{countryCode}}', )] +#[Assurance(type: 'string')] final class PostalCode extends Envelope { private const array POSTAL_CODES_EXTRA = [ diff --git a/src/Validators/Printable.php b/src/Validators/Printable.php index df6fa7a7b..60ba77d24 100644 --- a/src/Validators/Printable.php +++ b/src/Validators/Printable.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of printable characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Printable extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Property.php b/src/Validators/Property.php index 023ab0e71..273f11706 100644 --- a/src/Validators/Property.php +++ b/src/Validators/Property.php @@ -19,16 +19,18 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; +use Respect\Fluent\Attributes\ComposableParameter; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(prefix: 'property', prefixParameter: true, exclude: ['all', 'key', 'property'])] +#[Composable(prefix: self::class, without: [All::class, Key::class, self::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Property implements Validator { public function __construct( + #[ComposableParameter] private string $propertyName, private Validator $validator, ) { diff --git a/src/Validators/PropertyExists.php b/src/Validators/PropertyExists.php index efa55c00e..d207ece8d 100644 --- a/src/Validators/PropertyExists.php +++ b/src/Validators/PropertyExists.php @@ -14,7 +14,7 @@ use Attribute; use ReflectionClass; use ReflectionObject; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; @@ -22,7 +22,7 @@ use function is_object; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be present', diff --git a/src/Validators/PropertyOptional.php b/src/Validators/PropertyOptional.php index 56ed4f54e..519e948ef 100644 --- a/src/Validators/PropertyOptional.php +++ b/src/Validators/PropertyOptional.php @@ -13,11 +13,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property'])] +#[Composable(without: [All::class, Key::class, Property::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class PropertyOptional implements Validator { diff --git a/src/Validators/PublicDomainSuffix.php b/src/Validators/PublicDomainSuffix.php index 002f88d86..b82b073ee 100644 --- a/src/Validators/PublicDomainSuffix.php +++ b/src/Validators/PublicDomainSuffix.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -36,6 +37,7 @@ '{{subject}} must be a public domain suffix', '{{subject}} must not be a public domain suffix', )] +#[Assurance(type: 'string')] final class PublicDomainSuffix extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Punct.php b/src/Validators/Punct.php index b781f299d..a1e81face 100644 --- a/src/Validators/Punct.php +++ b/src/Validators/Punct.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of punctuation characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Punct extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Readable.php b/src/Validators/Readable.php index 641e50812..2424ecb31 100644 --- a/src/Validators/Readable.php +++ b/src/Validators/Readable.php @@ -17,6 +17,7 @@ use Attribute; use Psr\Http\Message\StreamInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -29,6 +30,7 @@ '{{subject}} must be readable', '{{subject}} must not be readable', )] +#[Assurance(type: ['string', SplFileInfo::class, StreamInterface::class])] final class Readable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Regex.php b/src/Validators/Regex.php index 679d277b3..562f90b60 100644 --- a/src/Validators/Regex.php +++ b/src/Validators/Regex.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -28,6 +29,7 @@ '{{subject}} must match the {{regex|quote}} pattern', '{{subject}} must not match the {{regex|quote}} pattern', )] +#[Assurance(type: 'string')] final readonly class Regex implements Validator { public function __construct( diff --git a/src/Validators/ResourceType.php b/src/Validators/ResourceType.php index 5fcd6a767..d78fa50a9 100644 --- a/src/Validators/ResourceType.php +++ b/src/Validators/ResourceType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be an internal resource', '{{subject}} must not be an internal resource', )] +#[Assurance(type: 'resource')] final class ResourceType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Roman.php b/src/Validators/Roman.php index d1c7959f2..73ea46b1a 100644 --- a/src/Validators/Roman.php +++ b/src/Validators/Roman.php @@ -18,6 +18,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -26,6 +27,7 @@ '{{subject}} must be a Roman numeral', '{{subject}} must not be a Roman numeral', )] +#[Assurance(type: 'string')] final class Roman extends Envelope { public function __construct() diff --git a/src/Validators/ScalarVal.php b/src/Validators/ScalarVal.php index 2edbcefd1..e7f2fc9d7 100644 --- a/src/Validators/ScalarVal.php +++ b/src/Validators/ScalarVal.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a scalar', '{{subject}} must not be a scalar', )] +#[Assurance(type: 'int|float|bool|string')] final class ScalarVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Slug.php b/src/Validators/Slug.php index dfd9f2b7e..f849f965a 100644 --- a/src/Validators/Slug.php +++ b/src/Validators/Slug.php @@ -19,6 +19,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -31,6 +32,7 @@ '{{subject}} must be a slug', '{{subject}} must not be a slug', )] +#[Assurance(type: 'string')] final class Slug extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Sorted.php b/src/Validators/Sorted.php index b8debd798..222783c71 100644 --- a/src/Validators/Sorted.php +++ b/src/Validators/Sorted.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -35,6 +36,7 @@ '{{subject}} must not be sorted in descending order', self::TEMPLATE_DESCENDING, )] +#[Assurance(type: 'array|string')] final readonly class Sorted implements Validator { public const string TEMPLATE_ASCENDING = '__ascending__'; diff --git a/src/Validators/Space.php b/src/Validators/Space.php index b39cb6503..96752df7b 100644 --- a/src/Validators/Space.php +++ b/src/Validators/Space.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -32,6 +33,7 @@ '{{subject}} must not consist only of space characters or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Space extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/Spaced.php b/src/Validators/Spaced.php index b8c020d4c..a407b2a76 100644 --- a/src/Validators/Spaced.php +++ b/src/Validators/Spaced.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must contain at least one whitespace', '{{subject}} must not contain whitespace', )] +#[Assurance(type: 'string')] final class Spaced extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/StringType.php b/src/Validators/StringType.php index 50bc93356..e9d456c2a 100644 --- a/src/Validators/StringType.php +++ b/src/Validators/StringType.php @@ -14,6 +14,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -24,6 +25,7 @@ '{{subject}} must be a string', '{{subject}} must not be a string', )] +#[Assurance(type: 'string')] final class StringType extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/StringVal.php b/src/Validators/StringVal.php index 9126ce561..4c2270a02 100644 --- a/src/Validators/StringVal.php +++ b/src/Validators/StringVal.php @@ -17,8 +17,10 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; +use Stringable; use function is_object; use function is_scalar; @@ -29,6 +31,7 @@ '{{subject}} must be a string', '{{subject}} must not be a string', )] +#[Assurance(type: ['scalar', Stringable::class])] final class StringVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/SubdivisionCode.php b/src/Validators/SubdivisionCode.php index da3c721f1..21fb99083 100644 --- a/src/Validators/SubdivisionCode.php +++ b/src/Validators/SubdivisionCode.php @@ -13,6 +13,7 @@ use Attribute; use Psr\Container\NotFoundExceptionInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -28,6 +29,7 @@ '{{subject}} must be a subdivision code of {{countryName|trans}}', '{{subject}} must not be a subdivision code of {{countryName|trans}}', )] +#[Assurance(type: 'string')] final readonly class SubdivisionCode implements Validator { use CanValidateUndefined; diff --git a/src/Validators/Subset.php b/src/Validators/Subset.php index dbde4cdf0..1897663b8 100644 --- a/src/Validators/Subset.php +++ b/src/Validators/Subset.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -29,6 +30,7 @@ '{{subject}} must be subset of {{superset}}', '{{subject}} must not be subset of {{superset}}', )] +#[Assurance(type: 'array')] final readonly class Subset implements Validator { /** @param mixed[] $superset */ diff --git a/src/Validators/SymbolicLink.php b/src/Validators/SymbolicLink.php index fc7513260..6b8aa619e 100644 --- a/src/Validators/SymbolicLink.php +++ b/src/Validators/SymbolicLink.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -27,6 +28,7 @@ '{{subject}} must be an accessible existing symbolic link', '{{subject}} must not be an accessible existing symbolic link', )] +#[Assurance(type: ['string', SplFileInfo::class])] final class SymbolicLink extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Templated.php b/src/Validators/Templated.php index 4e02ff847..45fdc4ddd 100644 --- a/src/Validators/Templated.php +++ b/src/Validators/Templated.php @@ -12,11 +12,11 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable(without: [All::class, Key::class, Property::class, Not::class, NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final readonly class Templated implements Validator { diff --git a/src/Validators/Time.php b/src/Validators/Time.php index 297c4439a..47799a85c 100644 --- a/src/Validators/Time.php +++ b/src/Validators/Time.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Helpers\CanValidateDateTime; use Respect\Validation\Message\Template; @@ -33,6 +34,7 @@ '{{subject}} must be a time in the {{sample}} format', '{{subject}} must not be a time in the {{sample}} format', )] +#[Assurance(type: 'string')] final readonly class Time implements Validator { use CanValidateDateTime; diff --git a/src/Validators/Tld.php b/src/Validators/Tld.php index 9221bed8a..af9f52a94 100644 --- a/src/Validators/Tld.php +++ b/src/Validators/Tld.php @@ -20,6 +20,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; @@ -29,6 +30,7 @@ '{{subject}} must be a top-level domain name', '{{subject}} must not be a top-level domain name', )] +#[Assurance(type: 'string')] final class Tld extends Envelope { public function __construct() diff --git a/src/Validators/Trimmed.php b/src/Validators/Trimmed.php index 88588aa1b..d3ef59c5a 100644 --- a/src/Validators/Trimmed.php +++ b/src/Validators/Trimmed.php @@ -11,6 +11,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Envelope; @@ -26,6 +27,7 @@ '{{subject}} must contain leading or trailing {{trimValues|list:or}}', self::TEMPLATE_CUSTOM, )] +#[Assurance(type: 'string')] final class Trimmed extends Envelope { public const string TEMPLATE_CUSTOM = '__custom__'; diff --git a/src/Validators/TrueVal.php b/src/Validators/TrueVal.php index 7d99308f7..2c21fffcc 100644 --- a/src/Validators/TrueVal.php +++ b/src/Validators/TrueVal.php @@ -13,6 +13,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -26,6 +27,7 @@ '{{subject}} must evaluate to `true`', '{{subject}} must not evaluate to `true`', )] +#[Assurance(type: 'true')] final class TrueVal extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Undef.php b/src/Validators/Undef.php index 2ea85a48a..19905afaa 100644 --- a/src/Validators/Undef.php +++ b/src/Validators/Undef.php @@ -16,13 +16,13 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; -#[Mixin(exclude: ['nullOr', 'undefOr'])] +#[Composable(without: [NullOr::class, UndefOr::class])] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be undefined', diff --git a/src/Validators/UndefOr.php b/src/Validators/UndefOr.php index 576fd2def..fcffbef11 100644 --- a/src/Validators/UndefOr.php +++ b/src/Validators/UndefOr.php @@ -13,7 +13,9 @@ namespace Respect\Validation\Validators; use Attribute; -use Respect\Dev\CodeGen\FluentBuilder\Mixin; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceModifier; +use Respect\Fluent\Attributes\Composable; use Respect\Validation\Helpers\CanValidateUndefined; use Respect\Validation\Message\Template; use Respect\Validation\Result; @@ -21,12 +23,16 @@ use function array_map; -#[Mixin(prefix: 'undefOr', exclude: ['all', 'key', 'property', 'not', 'nullOr', 'undefOr'])] +#[Composable( + prefix: self::class, + without: [All::class, Key::class, Property::class, Not::class, NullOr::class, self::class], +)] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( 'or must be undefined', 'and must not be undefined', )] +#[Assurance(modifier: AssuranceModifier::Nullable)] final readonly class UndefOr implements Validator { use CanValidateUndefined; diff --git a/src/Validators/Unique.php b/src/Validators/Unique.php index 724539bb5..c31fb7686 100644 --- a/src/Validators/Unique.php +++ b/src/Validators/Unique.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -29,6 +30,7 @@ '{{subject}} must not contain duplicates', '{{subject}} must contain duplicates', )] +#[Assurance(type: 'array')] final class Unique extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Uppercase.php b/src/Validators/Uppercase.php index 5ae7785f0..42bb6a418 100644 --- a/src/Validators/Uppercase.php +++ b/src/Validators/Uppercase.php @@ -16,6 +16,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must consist only of uppercase letters', '{{subject}} must not consist only of uppercase letters', )] +#[Assurance(type: 'string')] final class Uppercase extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Url.php b/src/Validators/Url.php index 92600a2ab..456d1cdbb 100644 --- a/src/Validators/Url.php +++ b/src/Validators/Url.php @@ -12,6 +12,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; @@ -26,6 +27,7 @@ '{{subject}} must be a URL', '{{subject}} must not be a URL', )] +#[Assurance(type: 'string')] final class Url implements Validator { private readonly Validator $validator; diff --git a/src/Validators/Uuid.php b/src/Validators/Uuid.php index 654df0a0c..c7d59bb67 100644 --- a/src/Validators/Uuid.php +++ b/src/Validators/Uuid.php @@ -22,6 +22,7 @@ use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\ContainerRegistry; use Respect\Validation\Exceptions\InvalidValidatorException; use Respect\Validation\Exceptions\MissingComposerDependencyException; @@ -43,6 +44,7 @@ '{{subject}} must not be a UUID v{{version|raw}}', self::TEMPLATE_VERSION, )] +#[Assurance(type: 'string')] final class Uuid implements Validator { public const string TEMPLATE_VERSION = '__version__'; diff --git a/src/Validators/Version.php b/src/Validators/Version.php index 62d311db6..7b2343a6a 100644 --- a/src/Validators/Version.php +++ b/src/Validators/Version.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -27,6 +28,7 @@ '{{subject}} must be a version number', '{{subject}} must not be a version number', )] +#[Assurance(type: 'string')] final class Version extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Vowel.php b/src/Validators/Vowel.php index 699e9695e..e5e67b66f 100644 --- a/src/Validators/Vowel.php +++ b/src/Validators/Vowel.php @@ -17,6 +17,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -33,6 +34,7 @@ '{{subject}} must not consist of vowels or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Vowel extends FilteredString { protected function isValid(string $input): bool diff --git a/src/Validators/When.php b/src/Validators/When.php index 75cbcd7ef..8f7fa226f 100644 --- a/src/Validators/When.php +++ b/src/Validators/When.php @@ -17,10 +17,13 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; +use Respect\Fluent\Attributes\AssuranceCompose; use Respect\Validation\Result; use Respect\Validation\Validator; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +#[Assurance(compose: AssuranceCompose::Union, composeRange: [1, null])] final readonly class When implements Validator { public function __construct( diff --git a/src/Validators/Writable.php b/src/Validators/Writable.php index 6f33716a0..221c3d0f2 100644 --- a/src/Validators/Writable.php +++ b/src/Validators/Writable.php @@ -17,6 +17,7 @@ use Attribute; use Psr\Http\Message\StreamInterface; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; use SplFileInfo; @@ -29,6 +30,7 @@ '{{subject}} must be an accessible existing writable filesystem entry', '{{subject}} must not be an accessible existing writable filesystem entry', )] +#[Assurance(type: ['string', SplFileInfo::class, StreamInterface::class])] final class Writable extends Simple { public function isValid(mixed $input): bool diff --git a/src/Validators/Xdigit.php b/src/Validators/Xdigit.php index 4544c0632..1a654ab73 100644 --- a/src/Validators/Xdigit.php +++ b/src/Validators/Xdigit.php @@ -15,6 +15,7 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Fluent\Attributes\Assurance; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\FilteredString; @@ -31,6 +32,7 @@ '{{subject}} must not consist only of hexadecimal digits or {{additionalChars}}', self::TEMPLATE_EXTRA, )] +#[Assurance(type: 'string')] final class Xdigit extends FilteredString { protected function isValid(string $input): bool diff --git a/tests/benchmark/PrefixBench.php b/tests/benchmark/PrefixBench.php deleted file mode 100644 index 233660fc4..000000000 --- a/tests/benchmark/PrefixBench.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - -declare(strict_types=1); - -namespace Respect\Validation\Benchmarks; - -use PhpBench\Attributes as Bench; -use Respect\Validation\Transformers\Prefix; -use Respect\Validation\Transformers\ValidatorSpec; - -final class PrefixBench -{ - /** @param array{0: Prefix, 1: ValidatorSpec} $params */ - #[Bench\ParamProviders(['provideTransformerSpec'])] - #[Bench\Iterations(10)] - #[Bench\RetryThreshold(5)] - #[Bench\Revs(100)] - #[Bench\Warmup(1)] - #[Bench\Subject] - public function prefixTransformer(array $params): void - { - $params[0]->transform($params[1]); - } - - /** @return array */ - public static function provideTransformerSpec(): array - { - return [ - [new Prefix(), new ValidatorSpec('keyName', ['value', 'other'])], - [new Prefix(), new ValidatorSpec('propertyTitle', ['value', 'other'])], - [new Prefix(), new ValidatorSpec('notSomething', ['value'])], - [new Prefix(), new ValidatorSpec('not')], - [new Prefix(), new ValidatorSpec('arrayVal')], - ]; - } -} diff --git a/tests/inference/NarrowingTest.php b/tests/inference/NarrowingTest.php new file mode 100644 index 000000000..3fcfb1aa2 --- /dev/null +++ b/tests/inference/NarrowingTest.php @@ -0,0 +1,39 @@ + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../vendor/respect/fluent-analysis/extension.neon', + __DIR__ . '/../../fluent.neon', + ]; + } + + /** @return iterable */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/assertions/narrowing.php'); + } + + #[Test] + #[DataProvider('dataFileAsserts')] + public function fileAsserts(string $assertType, string $file, mixed ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } +} diff --git a/tests/inference/assertions/narrowing.php b/tests/inference/assertions/narrowing.php new file mode 100644 index 000000000..df2d62468 --- /dev/null +++ b/tests/inference/assertions/narrowing.php @@ -0,0 +1,475 @@ +assert($x); + assertType('int', $x); +} + +function stringTypeAssert(mixed $x): void +{ + v::stringType()->assert($x); + assertType('string', $x); +} + +function floatTypeAssert(mixed $x): void +{ + v::floatType()->assert($x); + assertType('float', $x); +} + +function boolTypeAssert(mixed $x): void +{ + v::boolType()->assert($x); + assertType('bool', $x); +} + +function nullTypeAssert(mixed $x): void +{ + v::nullType()->assert($x); + assertType('null', $x); +} + +function arrayTypeAssert(mixed $x): void +{ + v::arrayType()->assert($x); + assertType('array', $x); +} + +function objectTypeAssert(mixed $x): void +{ + v::objectType()->assert($x); + assertType('object', $x); +} + +// --- Instance (dynamic) --- + +function instanceAssert(mixed $x): void +{ + v::instance(DateTimeInterface::class)->assert($x); + assertType('DateTimeInterface', $x); +} + +// --- Val validators --- + +function intValAssert(mixed $x): void +{ + v::intVal()->assert($x); + assertType('int|numeric-string', $x); +} + +function numericValAssert(mixed $x): void +{ + v::numericVal()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function scalarValAssert(mixed $x): void +{ + v::scalarVal()->assert($x); + assertType('bool|float|int|string', $x); +} + +// --- Composable prefixes --- + +function nullOrIntTypeAssert(mixed $x): void +{ + v::nullOrIntType()->assert($x); + assertType('int|null', $x); +} + +function notIntTypeAssert(int|string $x): void +{ + v::notIntType()->assert($x); + assertType('string', $x); +} + +// --- Chain intersection --- + +function chainIntersection(mixed $x): void +{ + v::intType()->positive()->assert($x); + assertType('int', $x); +} + +// --- check() works too --- + +function checkNarrowing(mixed $x): void +{ + v::stringType()->check($x); + assertType('string', $x); +} + +// --- isValid() type guard --- + +function isValidGuard(mixed $x): void +{ + if (!v::intType()->isValid($x)) { + return; + } + + assertType('int', $x); +} + +function isValidFalsey(int|string $x): void +{ + if (v::intType()->isValid($x)) { + return; + } + + assertType('string', $x); +} + +// --- P0: String format validators --- + +function emailAssert(mixed $x): void +{ + v::email()->assert($x); + assertType('string', $x); +} + +function uuidAssert(mixed $x): void +{ + v::uuid()->assert($x); + assertType('string', $x); +} + +function urlAssert(mixed $x): void +{ + v::url()->assert($x); + assertType('string', $x); +} + +function jsonAssert(mixed $x): void +{ + v::json()->assert($x); + assertType('string', $x); +} + +function alphaAssert(mixed $x): void +{ + v::alpha()->assert($x); + assertType('string', $x); +} + +function digitAssert(mixed $x): void +{ + v::digit()->assert($x); + assertType('string', $x); +} + +// --- P0: Filesystem validators --- + +function fileAssert(mixed $x): void +{ + v::file()->assert($x); + assertType('SplFileInfo|string', $x); +} + +function directoryAssert(mixed $x): void +{ + v::directory()->assert($x); + assertType('SplFileInfo|string', $x); +} + +// --- P0: Array validators --- + +function sortedAssert(mixed $x): void +{ + v::sorted('ASC')->assert($x); + assertType('array|string', $x); +} + +function uniqueAssert(mixed $x): void +{ + v::unique()->assert($x); + assertType('array', $x); +} + +// --- P0: Numeric validators --- + +function multipleAssert(mixed $x): void +{ + v::multiple(3)->assert($x); + assertType('int', $x); +} + +// --- P0: DateTime --- + +function dateTimeAssert(mixed $x): void +{ + v::dateTime()->assert($x); + assertType('DateTimeInterface|string', $x); +} + +// --- P0: Composable prefix + new narrowing --- + +function nullOrEmailAssert(mixed $x): void +{ + v::nullOrEmail()->assert($x); + assertType('string|null', $x); +} + +// --- P0: Chain with string format --- + +function stringTypeEmailChain(mixed $x): void +{ + v::stringType()->email()->assert($x); + assertType('string', $x); +} + +// --- Identical (value mode) --- + +function identicalIntAssert(mixed $x): void +{ + v::identical(42)->assert($x); + assertType('42', $x); +} + +function identicalStringAssert(mixed $x): void +{ + v::identical('foo')->assert($x); + assertType("'foo'", $x); +} + +// --- In (member mode) --- + +function inArrayAssert(mixed $x): void +{ + v::in(['active', 'inactive', 'pending'])->assert($x); + assertType("'active'|'inactive'|'pending'", $x); +} + +function inIntArrayAssert(mixed $x): void +{ + v::in([1, 2, 3])->assert($x); + assertType('1|2|3', $x); +} + +// --- AnyOf (children union) --- + +function anyOfAssert(mixed $x): void +{ + v::anyOf(v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +// --- AllOf (children intersect) --- + +function allOfAssert(mixed $x): void +{ + v::allOf(v::intType(), v::positive())->assert($x); + assertType('int', $x); +} + +// --- Each (elements) --- + +function eachAssert(mixed $x): void +{ + v::each(v::intType())->assert($x); + assertType('array', $x); +} + +function eachStringAssert(mixed $x): void +{ + v::each(v::email())->assert($x); + assertType('array', $x); +} + +// --- When (childrenRange) --- + +function whenAssert(mixed $x): void +{ + v::when(v::intType(), v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +// --- NoneOf (children union + remove) --- + +function noneOfAssert(int|string|float $x): void +{ + v::noneOf(v::intType(), v::stringType())->assert($x); + assertType('float', $x); +} + +// --- Additional type validators --- + +function callableTypeAssert(mixed $x): void +{ + v::callableType()->assert($x); + assertType('callable(): mixed', $x); +} + +function resourceTypeAssert(mixed $x): void +{ + v::resourceType()->assert($x); + assertType('resource', $x); +} + +function iterableTypeAssert(mixed $x): void +{ + v::iterableType()->assert($x); + assertType('iterable', $x); +} + +// --- Bool variants --- + +function trueValAssert(mixed $x): void +{ + v::trueVal()->assert($x); + assertType('true', $x); +} + +function falseValAssert(mixed $x): void +{ + v::falseVal()->assert($x); + assertType('false', $x); +} + +function boolValAssert(mixed $x): void +{ + v::boolVal()->assert($x); + assertType('bool', $x); +} + +// --- Val variants --- + +function floatValAssert(mixed $x): void +{ + v::floatVal()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function stringValAssert(mixed $x): void +{ + v::stringVal()->assert($x); + assertType('bool|float|int|string|Stringable', $x); +} + +function arrayValAssert(mixed $x): void +{ + v::arrayVal()->assert($x); + assertType('array|ArrayAccess', $x); +} + +function iterableValAssert(mixed $x): void +{ + v::iterableVal()->assert($x); + assertType('array|stdClass|Traversable', $x); +} + +// --- Collection --- + +function countableAssert(mixed $x): void +{ + v::countable()->assert($x); + assertType('array|Countable', $x); +} + +// --- Numeric --- + +function positiveAssert(mixed $x): void +{ + v::positive()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function negativeAssert(mixed $x): void +{ + v::negative()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function evenAssert(mixed $x): void +{ + v::even()->assert($x); + assertType('int', $x); +} + +function oddAssert(mixed $x): void +{ + v::odd()->assert($x); + assertType('int', $x); +} + +function factorAssert(mixed $x): void +{ + v::factor(10)->assert($x); + assertType('int', $x); +} + +function finiteAssert(mixed $x): void +{ + v::finite()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function infiniteAssert(mixed $x): void +{ + v::infinite()->assert($x); + assertType('float|int|numeric-string', $x); +} + +function numberAssert(mixed $x): void +{ + v::number()->assert($x); + assertType('float|int|numeric-string', $x); +} + +// --- Date/Time --- + +function dateAssert(mixed $x): void +{ + v::date()->assert($x); + assertType('string', $x); +} + +function timeAssert(mixed $x): void +{ + v::time()->assert($x); + assertType('string', $x); +} + +function leapYearAssert(mixed $x): void +{ + v::leapYear()->assert($x); + assertType('int|string', $x); +} + +// --- Composites --- + +function oneOfAssert(mixed $x): void +{ + v::oneOf(v::intType(), v::stringType())->assert($x); + assertType('int|string', $x); +} + +function allAssert(mixed $x): void +{ + v::all(v::intType())->assert($x); + assertType('array', $x); +} + +// --- Modifier: undefOr --- + +function undefOrIntTypeAssert(mixed $x): void +{ + v::undefOrIntType()->assert($x); + assertType('int|null', $x); +} diff --git a/tests/src/Transformers/StubTransformer.php b/tests/src/Transformers/StubTransformer.php deleted file mode 100644 index caea9ec3c..000000000 --- a/tests/src/Transformers/StubTransformer.php +++ /dev/null @@ -1,23 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Test\Transformers; - -use Respect\Validation\Transformers\Transformer; -use Respect\Validation\Transformers\ValidatorSpec; - -final class StubTransformer implements Transformer -{ - public function transform(ValidatorSpec $validatorSpec): ValidatorSpec - { - return $validatorSpec; - } -} diff --git a/src-dev/CodeGen/CodeGenerator.php b/tests/src/Validators/NonPublic.php similarity index 57% rename from src-dev/CodeGen/CodeGenerator.php rename to tests/src/Validators/NonPublic.php index 69007f32d..e2ea2fc25 100644 --- a/src-dev/CodeGen/CodeGenerator.php +++ b/tests/src/Validators/NonPublic.php @@ -8,10 +8,11 @@ declare(strict_types=1); -namespace Respect\Dev\CodeGen; +namespace Respect\Validation\Test\Validators; -interface CodeGenerator +final class NonPublic { - /** @return array filename => content */ - public function generate(): array; + private function __construct() + { + } } diff --git a/tests/unit/ContainerRegistryTest.php b/tests/unit/ContainerRegistryTest.php index f0d8c6674..1dd9feac3 100644 --- a/tests/unit/ContainerRegistryTest.php +++ b/tests/unit/ContainerRegistryTest.php @@ -50,4 +50,21 @@ public function itAllowsOverwritingTheContainer(): void self::assertSame($newContainer, ContainerRegistry::getContainer()); } + + #[Test] + public function extraNamespacesResolveCustomValidators(): void + { + $mainContainer = ContainerRegistry::getContainer(); + ContainerRegistry::setContainer(ContainerRegistry::createContainer([ + 'respect.validation.rule_factory.namespaces' => ['Respect\\Validation\\Test\\Validators'], + ])); + + try { + // 'CustomRule' exists in Test\Validators but not in Validators + $builder = ValidatorBuilder::customRule(); // @phpstan-ignore staticMethod.notFound + self::assertCount(1, $builder->getValidators()); + } finally { + ContainerRegistry::setContainer($mainContainer); + } + } } diff --git a/tests/unit/NamespacedRuleFactoryTest.php b/tests/unit/NamespacedRuleFactoryTest.php deleted file mode 100644 index 3dc428190..000000000 --- a/tests/unit/NamespacedRuleFactoryTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - * SPDX-FileContributor: Augusto Pascutti - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\Exceptions\ComponentException; -use Respect\Validation\Exceptions\InvalidClassException; -use Respect\Validation\Test\TestCase; -use Respect\Validation\Test\Transformers\StubTransformer; -use Respect\Validation\Test\Validators\Invalid; -use Respect\Validation\Test\Validators\MyAbstractClass; -use Respect\Validation\Test\Validators\Stub; -use Respect\Validation\Test\Validators\Valid; - -use function assert; -use function sprintf; - -#[Group('core')] -#[CoversClass(NamespacedValidatorFactory::class)] -final class NamespacedRuleFactoryTest extends TestCase -{ - private const string TEST_RULES_NAMESPACE = 'Respect\\Validation\\Test\\Validators'; - - #[Test] - public function shouldCreateRuleByNameBasedOnNamespace(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - self::assertInstanceOf(Valid::class, $factory->create('valid')); - } - - #[Test] - public function shouldLookUpToAllNamespacesUntilRuleIsFound(): void - { - $factory = (new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE])) - ->withNamespace(__NAMESPACE__); - - self::assertInstanceOf(Valid::class, $factory->create('valid')); - } - - #[Test] - public function shouldDefineConstructorArgumentsWhenCreatingRule(): void - { - $constructorArguments = [true, false, true, false]; - - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - $validator = $factory->create('stub', $constructorArguments); - assert($validator instanceof Stub); - - self::assertSame($constructorArguments, $validator->validations); - } - - #[Test] - public function shouldThrowsAnExceptionOnConstructorReflectionFailure(): void - { - $constructorArguments = ['a', 'b']; - - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage('"noConstructor" could not be instantiated with arguments `["a", "b"]`'); - - $factory->create('noConstructor', $constructorArguments); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsInvalid(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage(sprintf('"%s" must be an instance of "%s"', Invalid::class, Validator::class)); - - $factory->create('invalid'); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsNotInstantiable(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(InvalidClassException::class); - $this->expectExceptionMessage(sprintf('"%s" must be instantiable', MyAbstractClass::class)); - - $factory->create('myAbstractClass'); - } - - #[Test] - public function shouldThrowsAnExceptionWhenRuleIsNotFound(): void - { - $factory = new NamespacedValidatorFactory(new StubTransformer(), [self::TEST_RULES_NAMESPACE]); - - $this->expectException(ComponentException::class); - $this->expectExceptionMessage('"nonExistingRule" is not a valid rule name'); - - $factory->create('nonExistingRule'); - } -} diff --git a/tests/unit/Transformers/PrefixTest.php b/tests/unit/Transformers/PrefixTest.php deleted file mode 100644 index 9fedb4d58..000000000 --- a/tests/unit/Transformers/PrefixTest.php +++ /dev/null @@ -1,102 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Transformers; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\Test\TestCase; - -#[Group('core')] -#[CoversClass(Prefix::class)] -final class PrefixTest extends TestCase -{ - #[Test] - #[DataProvider('providerForTransformedValidatorSpec')] - public function itShouldTransformValidatorSpec(ValidatorSpec $original, ValidatorSpec $expected): void - { - $transformer = new Prefix(); - $transformed = $transformer->transform($original); - - self::assertEquals($expected, $transformed); - } - - #[Test] - #[DataProvider('providerForUntransformedRuleNames')] - public function itShouldPreventTransformingCanonicalRule(string $ruleName): void - { - $validatorSpec = new ValidatorSpec($ruleName); - - $transformer = new Prefix(); - self::assertSame($validatorSpec, $transformer->transform($validatorSpec)); - } - - /** @return array */ - public static function providerForTransformedValidatorSpec(): array - { - return [ - 'key' => [ - new ValidatorSpec('keyNextRule', ['keyName', 123]), - new ValidatorSpec('NextRule', [123], new ValidatorSpec('key', ['keyName'])), - ], - 'length' => [ - new ValidatorSpec('lengthNextRule', [5]), - new ValidatorSpec('NextRule', [5], new ValidatorSpec('length')), - ], - 'max' => [ - new ValidatorSpec('maxNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('max')), - ], - 'min' => [ - new ValidatorSpec('minNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('min')), - ], - 'not' => [ - new ValidatorSpec('notNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('not')), - ], - 'nullOr' => [ - new ValidatorSpec('nullOrNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('nullOr')), - ], - 'property' => [ - new ValidatorSpec('propertyNextRule', ['propertyName', 567]), - new ValidatorSpec('NextRule', [567], new ValidatorSpec('property', ['propertyName'])), - ], - 'undefOr' => [ - new ValidatorSpec('undefOrNextRule', [1, 10]), - new ValidatorSpec('NextRule', [1, 10], new ValidatorSpec('undefOr')), - ], - ]; - } - - /** @return array */ - public static function providerForUntransformedRuleNames(): array - { - return [ - 'equals' => ['equals'], - 'key' => ['key'], - 'keyExists' => ['keyExists'], - 'keyOptional' => ['keyOptional'], - 'keySet' => ['keySet'], - 'length' => ['length'], - 'max' => ['max'], - 'min' => ['min'], - 'not' => ['not'], - 'undef' => ['undef'], - 'property' => ['property'], - 'propertyExists' => ['propertyExists'], - 'propertyOptional' => ['propertyOptional'], - ]; - } -} diff --git a/tests/unit/ValidatorBuilderTest.php b/tests/unit/ValidatorBuilderTest.php index 3f152a573..d17642362 100644 --- a/tests/unit/ValidatorBuilderTest.php +++ b/tests/unit/ValidatorBuilderTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\Test; +use Respect\Fluent\Exceptions\CouldNotResolve; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\ValidationException; use Respect\Validation\Test\TestCase; @@ -31,7 +32,7 @@ final class ValidatorBuilderTest extends TestCase #[Test] public function invalidRuleClassShouldThrowComponentException(): void { - $this->expectException(ComponentException::class); + $this->expectException(CouldNotResolve::class); // @phpstan-ignore-next-line ValidatorBuilder::iDoNotExistSoIShouldThrowException(); @@ -42,7 +43,6 @@ public function shouldReturnValidatorInstanceWhenTheNotRuleIsCalledWithArguments { $validator = ValidatorBuilder::init(); - // @phpstan-ignore-next-line self::assertNotSame($validator, $validator->not($validator->falsy())); }