From 8bdc4a719c2ababcd2bae03cd359ef73002b1859 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Sun, 1 Feb 2026 13:32:40 +0100 Subject: [PATCH] Create ShortCircuit validator and ShortCircuitCapable interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a mechanism for validators to return early once the validation outcome is determined, rather than evaluating all child validators. The ShortCircuit validator evaluates validators sequentially and stops at the first failure, similar to how PHP's && operator works. This is useful when later validators depend on earlier ones passing, or when you want only the first error message. The ShortCircuitCapable interface allows composite validators (AllOf, AnyOf, OneOf, NoneOf, Each, All) to implement their own short-circuit logic. Why "ShortCircuit" instead of "FailFast": The name "FailFast" was initially considered but proved misleading. While AllOf stops on failure (fail fast), AnyOf stops on success (succeed fast), and OneOf stops on the second success. The common behavior is not about failing quickly, but about returning as soon as the outcome is determined—which is exactly what short-circuit evaluation means. This terminology is familiar to developers from boolean operators (&& and ||), making the behavior immediately understandable. Co-authored-by: Alexandre Gomes Gaigalas Assisted-by: Claude Code (Opus 4.5) --- docs/feature-guide.md | 2 +- docs/migrating-from-v2-to-v3.md | 42 +++--- docs/validators.md | 10 +- docs/validators/After.md | 6 +- docs/validators/AllOf.md | 2 +- docs/validators/AnyOf.md | 2 +- docs/validators/Circuit.md | 70 ---------- docs/validators/Factory.md | 2 +- docs/validators/NoneOf.md | 2 +- docs/validators/OneOf.md | 2 +- docs/validators/ShortCircuit.md | 78 +++++++++++ docs/validators/SubdivisionCode.md | 2 +- docs/validators/When.md | 2 +- src/Helpers/CanEvaluateShortCircuit.php | 27 ++++ src/Mixins/AllBuilder.php | 4 +- src/Mixins/AllChain.php | 4 +- src/Mixins/Builder.php | 4 +- src/Mixins/Chain.php | 4 +- src/Mixins/KeyBuilder.php | 4 +- src/Mixins/KeyChain.php | 4 +- src/Mixins/NotBuilder.php | 4 +- src/Mixins/NotChain.php | 4 +- src/Mixins/NullOrBuilder.php | 4 +- src/Mixins/NullOrChain.php | 4 +- src/Mixins/PropertyBuilder.php | 4 +- src/Mixins/PropertyChain.php | 4 +- src/Mixins/UndefOrBuilder.php | 4 +- src/Mixins/UndefOrChain.php | 4 +- src/ValidatorBuilder.php | 56 +++++--- src/Validators/All.php | 29 +++- src/Validators/AllOf.php | 20 ++- src/Validators/AnyOf.php | 20 ++- src/Validators/Core/ShortCircuitable.php | 19 +++ src/Validators/Domain.php | 8 +- src/Validators/Each.php | 28 +++- src/Validators/KeySet.php | 38 ++++- src/Validators/NoneOf.php | 21 ++- src/Validators/OneOf.php | 26 +++- .../{Circuit.php => ShortCircuit.php} | 17 ++- src/Validators/Tld.php | 2 +- src/Validators/Url.php | 6 +- tests/benchmark/CompositeValidatorsBench.php | 121 ++++++++++++++++ tests/feature/Validators/AllOfTest.php | 21 +++ tests/feature/Validators/AnyOfTest.php | 56 ++++++++ tests/feature/Validators/EachTest.php | 34 +++++ tests/feature/Validators/KeySetTest.php | 131 ++++++++++++++++++ tests/feature/Validators/NoneOfTest.php | 24 ++++ .../{CircuitTest.php => ShortCircuitTest.php} | 46 +++--- tests/src/SmokeTestProvider.php | 2 +- tests/src/Validators/Stub.php | 2 +- tests/unit/Validators/AllOfTest.php | 53 ++++++- tests/unit/Validators/AllTest.php | 72 ++++++++++ tests/unit/Validators/AnyOfTest.php | 50 ++++++- tests/unit/Validators/EachTest.php | 85 ++++++++++++ tests/unit/Validators/KeySetTest.php | 47 +++++++ tests/unit/Validators/NoneOfTest.php | 53 ++++++- tests/unit/Validators/OneOfTest.php | 50 ++++++- .../{CircuitTest.php => ShortCircuitTest.php} | 10 +- 58 files changed, 1246 insertions(+), 206 deletions(-) delete mode 100644 docs/validators/Circuit.md create mode 100644 docs/validators/ShortCircuit.md create mode 100644 src/Helpers/CanEvaluateShortCircuit.php create mode 100644 src/Validators/Core/ShortCircuitable.php rename src/Validators/{Circuit.php => ShortCircuit.php} (58%) create mode 100644 tests/benchmark/CompositeValidatorsBench.php rename tests/feature/Validators/{CircuitTest.php => ShortCircuitTest.php} (65%) rename tests/unit/Validators/{CircuitTest.php => ShortCircuitTest.php} (84%) diff --git a/docs/feature-guide.md b/docs/feature-guide.md index 1142b512c..b48ad5882 100644 --- a/docs/feature-guide.md +++ b/docs/feature-guide.md @@ -129,7 +129,7 @@ Beyond the examples above, Respect\Validation provides specialized validators fo - **Grouped validation**: Combine validators with AND/OR logic using [AllOf](validators/AllOf.md), [AnyOf](validators/AnyOf.md), [NoneOf](validators/NoneOf.md), [OneOf](validators/OneOf.md). - **Iteration**: Validate every item in a collection with [Each](validators/Each.md). - **Length, Min, Max**: Validate derived values with [Length](validators/Length.md), [Min](validators/Min.md), [Max](validators/Max.md). -- **Special cases**: Handle dynamic rules with [Factory](validators/Factory.md), short-circuit on first failure with [Circuit](validators/Circuit.md), or transform input before validation with [After](validators/After.md). +- **Special cases**: Handle dynamic rules with [Factory](validators/Factory.md), short-circuit on first failure with [ShortCircuit](validators/ShortCircuit.md), or transform input before validation with [After](validators/After.md). ## Customizing error messages diff --git a/docs/migrating-from-v2-to-v3.md b/docs/migrating-from-v2-to-v3.md index 9d971b644..c4bb9fe02 100644 --- a/docs/migrating-from-v2-to-v3.md +++ b/docs/migrating-from-v2-to-v3.md @@ -587,9 +587,9 @@ Version 3.0 introduces several new validators: | `All` | Validates that every item in an iterable passes validation | | `Attributes` | Validates object properties using PHP attributes | | `BetweenExclusive` | Validates that a value is between two bounds (exclusive) | -| `Circuit` | Short-circuit validation, stops at first failure | | `ContainsCount` | Validates the count of occurrences in a value | | `DateTimeDiff` | Validates date/time differences (replaces Age validators) | +| `ShortCircuit` | Stops at first failure instead of collecting all errors | | `Hetu` | Validates Finnish personal identity codes (henkilötunnus) | | `KeyExists` | Checks if an array key exists | | `KeyOptional` | Validates an array key only if it exists | @@ -645,26 +645,6 @@ v::betweenExclusive(1, 10)->assert(1); // fails (1 is not > 1) v::betweenExclusive(1, 10)->assert(10); // fails (10 is not < 10) ``` -#### Circuit - -Validates input against a series of validators, stopping at the first failure. Useful for dependent validations: - -```php -$validator = v::circuit( - v::key('countryCode', v::countryCode()), - v::factory( - fn($input) => v::key( - 'subdivisionCode', - v::subdivisionCode($input['countryCode']) - ) - ), -); - -$validator->assert([]); // → `.countryCode` must be present -$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present -$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes -``` - #### ContainsCount Validates the count of occurrences of a value: @@ -683,6 +663,26 @@ v::dateTimeDiff('years', v::greaterThanOrEqual(18))->assert('2000-01-01'); // pa v::dateTimeDiff('days', v::lessThan(30))->assert('2024-01-15'); // passes if less than 30 days ago ``` +#### ShortCircuit + +Validates input against a series of validators, stopping at the first failure. Useful for dependent validations: + +```php +$validator = v::shortCircuit( + v::key('countryCode', v::countryCode()), + v::lazy( + fn($input) => v::key( + 'subdivisionCode', + v::subdivisionCode($input['countryCode']) + ) + ), +); + +$validator->assert([]); // → `.countryCode` must be present +$validator->assert(['countryCode' => 'US']); // → `.subdivisionCode` must be present +$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); // passes +``` + #### Hetu Validates Finnish personal identity codes (henkilötunnus): diff --git a/docs/validators.md b/docs/validators.md index c8a2559ff..929cb6507 100644 --- a/docs/validators.md +++ b/docs/validators.md @@ -17,9 +17,9 @@ In this page you will find a list of validators by their category. **Comparisons**: [All][] - [Between][] - [BetweenExclusive][] - [Equals][] - [Equivalent][] - [GreaterThan][] - [GreaterThanOrEqual][] - [Identical][] - [In][] - [Length][] - [LessThan][] - [LessThanOrEqual][] - [Max][] - [Min][] -**Composite**: [AllOf][] - [AnyOf][] - [Circuit][] - [NoneOf][] - [OneOf][] +**Composite**: [AllOf][] - [AnyOf][] - [NoneOf][] - [OneOf][] - [ShortCircuit][] -**Conditions**: [Circuit][] - [Not][] - [When][] +**Conditions**: [Not][] - [ShortCircuit][] - [When][] **Core**: [Named][] - [Not][] - [Templated][] @@ -41,7 +41,7 @@ In this page you will find a list of validators by their category. **Miscellaneous**: [Blank][] - [Falsy][] - [Masked][] - [Named][] - [Templated][] - [Undef][] -**Nesting**: [After][] - [AllOf][] - [AnyOf][] - [Circuit][] - [Each][] - [Factory][] - [Key][] - [KeySet][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [UndefOr][] - [When][] +**Nesting**: [After][] - [AllOf][] - [AnyOf][] - [Each][] - [Factory][] - [Key][] - [KeySet][] - [NoneOf][] - [Not][] - [NullOr][] - [OneOf][] - [Property][] - [PropertyOptional][] - [ShortCircuit][] - [UndefOr][] - [When][] **Numbers**: [Base][] - [Decimal][] - [Digit][] - [Even][] - [Factor][] - [Finite][] - [FloatType][] - [FloatVal][] - [Infinite][] - [IntType][] - [IntVal][] - [Multiple][] - [Negative][] - [Number][] - [NumericVal][] - [Odd][] - [Positive][] - [Roman][] @@ -78,7 +78,6 @@ In this page you will find a list of validators by their category. - [Bsn][] - `v::bsn()->assert('612890053');` - [CallableType][] - `v::callableType()->assert(function () {});` - [Charset][] - `v::charset('ASCII')->assert('sugar');` -- [Circuit][] - `v::circuit(v::intVal(), v::floatVal())->assert(15);` - [Cnh][] - `v::cnh()->assert('02650306461');` - [Cnpj][] - `v::cnpj()->assert('00394460005887');` - [Consonant][] - `v::consonant()->assert('xkcd');` @@ -186,6 +185,7 @@ In this page you will find a list of validators by their category. - [Roman][] - `v::roman()->assert('IV');` - [Satisfies][] - `v::satisfies(fn (int $input): bool => $input % 5 === 0,)->assert(10);` - [ScalarVal][] - `v::scalarVal()->assert(135.0);` +- [ShortCircuit][] - `v::shortCircuit(v::intVal(), v::positive())->assert(15);` - [Size][] - `v::size('KB', v::greaterThan(1))->assert('/path/to/file');` - [Slug][] - `v::slug()->assert('my-wordpress-title');` - [Sorted][] - `v::sorted('ASC')->assert([1, 2, 3]);` @@ -234,7 +234,6 @@ In this page you will find a list of validators by their category. [Bsn]: validators/Bsn.md "Validates a Dutch citizen service number (BSN)." [CallableType]: validators/CallableType.md "Validates whether the pseudo-type of the input is callable." [Charset]: validators/Charset.md "Validates if a string is in a specific charset." -[Circuit]: validators/Circuit.md "Validates the input against a series of validators until the first fails." [Cnh]: validators/Cnh.md "Validates a Brazilian driver's license." [Cnpj]: validators/Cnpj.md "Validates if the input is a Brazilian National Registry of Legal Entities (CNPJ) number." [Consonant]: validators/Consonant.md "Validates if the input contains only consonants." @@ -342,6 +341,7 @@ In this page you will find a list of validators by their category. [Roman]: validators/Roman.md "Validates if the input is a Roman numeral." [Satisfies]: validators/Satisfies.md "Validates the input using the return of a given callable." [ScalarVal]: validators/ScalarVal.md "Validates whether the input is a scalar value or not." +[ShortCircuit]: validators/ShortCircuit.md "Validates the input against a series of validators, stopping at the first failure." [Size]: validators/Size.md "Validates whether the input is a file that is of a certain size or not." [Slug]: validators/Slug.md "Validates whether the input is a valid slug." [Sorted]: validators/Sorted.md "Validates whether the input is sorted in a certain order or not." diff --git a/docs/validators/After.md b/docs/validators/After.md index 7fc9540df..6556c5d5c 100644 --- a/docs/validators/After.md +++ b/docs/validators/After.md @@ -53,17 +53,17 @@ v::after( ``` `After` does not handle possible errors (type mismatches). If you need to -ensure that your callback is of a certain type, use [Circuit](Circuit.md) or +ensure that your callback is of a certain type, use [ShortCircuit](ShortCircuit.md) or handle it using a closure: ```php v::after('strtolower', v::equals('ABC'))->assert(123); // 𝙭 strtolower(): Argument #1 ($string) must be of type string, int given -v::circuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert(123); +v::shortCircuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert(123); // → 123 must be a string -v::circuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert('ABC'); +v::shortCircuit(v::stringType(), v::after('strtolower', v::equals('abc')))->assert('ABC'); // Validation passes successfully ``` diff --git a/docs/validators/AllOf.md b/docs/validators/AllOf.md index fa5aaf9b7..4a6b9999e 100644 --- a/docs/validators/AllOf.md +++ b/docs/validators/AllOf.md @@ -56,7 +56,7 @@ Used when all validators have failed. ## See Also - [AnyOf](AnyOf.md) -- [Circuit](Circuit.md) - [NoneOf](NoneOf.md) - [OneOf](OneOf.md) +- [ShortCircuit](ShortCircuit.md) - [When](When.md) diff --git a/docs/validators/AnyOf.md b/docs/validators/AnyOf.md index dbad4db71..1bd3e61c1 100644 --- a/docs/validators/AnyOf.md +++ b/docs/validators/AnyOf.md @@ -50,8 +50,8 @@ so `AnyOf()` returns true. ## See Also - [AllOf](AllOf.md) -- [Circuit](Circuit.md) - [ContainsAny](ContainsAny.md) - [NoneOf](NoneOf.md) - [OneOf](OneOf.md) +- [ShortCircuit](ShortCircuit.md) - [When](When.md) diff --git a/docs/validators/Circuit.md b/docs/validators/Circuit.md deleted file mode 100644 index f06338872..000000000 --- a/docs/validators/Circuit.md +++ /dev/null @@ -1,70 +0,0 @@ - - -# Circuit - -- `Circuit(Validator $validator1, Validator $validator2)` -- `Circuit(Validator $validator1, Validator $validator2, Validator ...$validators)` - -Validates the input against a series of validators until the first fails. - -```php -v::circuit(v::intVal(), v::floatVal())->assert(15); -// Validation passes successfully -``` - -This validator can be handy for getting the least error messages possible from a chain. - -This validator can be helpful in combinations with [Factory](Factory.md). An excellent example is when you want to validate a -country code and a subdivision code. - -```php -$validator = v::circuit( - v::key('countryCode', v::countryCode()), - v::factory(static fn($input) => v::key('subdivisionCode', v::subdivisionCode($input['countryCode']))), -); - -$validator->assert([]); -// → `.countryCode` must be present - -$validator->assert(['countryCode' => 'US']); -// → `.subdivisionCode` must be present - -$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'ZZ']); -// → `.subdivisionCode` must be a subdivision code of United States - -$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); -// Validation passes successfully -``` - -You need a valid country code to create a [SubdivisionCode](SubdivisionCode.md), so it makes sense only to validate the -subdivision code only if the country code is valid. In this case, you could also have used [When](When.md), but you -would then have to write `v::key('countryCode', v::countryCode())` twice in your chain. - -## Templates - -This validator does not have any templates, because it will always return the result of the first validator that fails. When all the validators pass, it will return the result of the last validator of the circuit. - -## Categorization - -- Composite -- Conditions -- Nesting - -## Changelog - -| Version | Description | -| ------: | :---------- | -| 3.0.0 | Created | - -## See Also - -- [AllOf](AllOf.md) -- [AnyOf](AnyOf.md) -- [Factory](Factory.md) -- [NoneOf](NoneOf.md) -- [OneOf](OneOf.md) -- [SubdivisionCode](SubdivisionCode.md) -- [When](When.md) diff --git a/docs/validators/Factory.md b/docs/validators/Factory.md index 68923c65b..2105b7fd0 100644 --- a/docs/validators/Factory.md +++ b/docs/validators/Factory.md @@ -58,4 +58,4 @@ on the input itself (`$_POST`), but it will use any input that’s given to the - [After](After.md) - [CallableType](CallableType.md) -- [Circuit](Circuit.md) +- [ShortCircuit](ShortCircuit.md) diff --git a/docs/validators/NoneOf.md b/docs/validators/NoneOf.md index 204a66a2d..a384ceb52 100644 --- a/docs/validators/NoneOf.md +++ b/docs/validators/NoneOf.md @@ -59,7 +59,7 @@ Used when all validators have passed. - [AllOf](AllOf.md) - [AnyOf](AnyOf.md) -- [Circuit](Circuit.md) - [Not](Not.md) - [OneOf](OneOf.md) +- [ShortCircuit](ShortCircuit.md) - [When](When.md) diff --git a/docs/validators/OneOf.md b/docs/validators/OneOf.md index 5e75ac172..a471ce2d3 100644 --- a/docs/validators/OneOf.md +++ b/docs/validators/OneOf.md @@ -73,6 +73,6 @@ Used when more than one validator has passed. - [AllOf](AllOf.md) - [AnyOf](AnyOf.md) -- [Circuit](Circuit.md) - [NoneOf](NoneOf.md) +- [ShortCircuit](ShortCircuit.md) - [When](When.md) diff --git a/docs/validators/ShortCircuit.md b/docs/validators/ShortCircuit.md new file mode 100644 index 000000000..98e2a18a2 --- /dev/null +++ b/docs/validators/ShortCircuit.md @@ -0,0 +1,78 @@ + + +# ShortCircuit + +- `ShortCircuit()` +- `ShortCircuit(Validator ...$validators)` + +Validates the input against a series of validators, stopping at the first failure. + +Like PHP's `&&` operator, it uses short-circuit evaluation: once the outcome is determined, remaining validators are +skipped. Unlike [AllOf](AllOf.md), which evaluates all validators and collects all failures, `ShortCircuit` returns +immediately. + +```php +v::shortCircuit(v::intVal(), v::positive())->assert(15); +// Validation passes successfully +``` + +This is useful when: + +- You want only the first error message instead of all of them +- Later validators depend on earlier ones passing (e.g., checking a format before checking a value) +- You want to avoid unnecessary validation work + +This validator is particularly useful in combination with [Factory](Factory.md) when later validations depend on earlier +results. For example, validating a subdivision code that depends on a valid country code: + +```php +$validator = v::shortCircuit( + v::key('countryCode', v::countryCode()), + v::factory(static fn($input) => v::key('subdivisionCode', v::subdivisionCode($input['countryCode']))), +); + +$validator->assert([]); +// → `.countryCode` must be present + +$validator->assert(['countryCode' => 'US']); +// → `.subdivisionCode` must be present + +$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'ZZ']); +// → `.subdivisionCode` must be a subdivision code of United States + +$validator->assert(['countryCode' => 'US', 'subdivisionCode' => 'CA']); +// Validation passes successfully +``` + +Because [SubdivisionCode](SubdivisionCode.md) requires a valid country code, it only makes sense to validate the +subdivision after the country code passes. You could achieve this with [When](When.md), but you would have to repeat +`v::key('countryCode', v::countryCode())` twice. + +## Templates + +This validator does not have templates of its own. It returns the result of the first failing validator, or the result +of the last validator when all pass. + +## Categorization + +- Composite +- Conditions +- Nesting + +## Changelog + +| Version | Description | +| ------: | :---------- | +| 3.0.0 | Created | + +## See Also + +- [AllOf](AllOf.md) +- [AnyOf](AnyOf.md) +- [NoneOf](NoneOf.md) +- [OneOf](OneOf.md) +- [SubdivisionCode](SubdivisionCode.md) +- [When](When.md) diff --git a/docs/validators/SubdivisionCode.md b/docs/validators/SubdivisionCode.md index 302fa7b9b..821d3dcc4 100644 --- a/docs/validators/SubdivisionCode.md +++ b/docs/validators/SubdivisionCode.md @@ -51,13 +51,13 @@ v::subdivisionCode('US')->assert('CA'); ## See Also -- [Circuit](Circuit.md) - [CountryCode](CountryCode.md) - [CurrencyCode](CurrencyCode.md) - [Nip](Nip.md) - [Pesel](Pesel.md) - [PolishIdCard](PolishIdCard.md) - [PublicDomainSuffix](PublicDomainSuffix.md) +- [ShortCircuit](ShortCircuit.md) - [Tld](Tld.md) [ISO 3166-1 alpha-2]: http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 "ISO 3166-1 alpha-2" diff --git a/docs/validators/When.md b/docs/validators/When.md index 90b99f990..d3d2e0c68 100644 --- a/docs/validators/When.md +++ b/docs/validators/When.md @@ -56,6 +56,6 @@ When `$else` is not defined use [AlwaysInvalid](AlwaysInvalid.md) - [AllOf](AllOf.md) - [AlwaysInvalid](AlwaysInvalid.md) - [AnyOf](AnyOf.md) -- [Circuit](Circuit.md) - [NoneOf](NoneOf.md) - [OneOf](OneOf.md) +- [ShortCircuit](ShortCircuit.md) diff --git a/src/Helpers/CanEvaluateShortCircuit.php b/src/Helpers/CanEvaluateShortCircuit.php new file mode 100644 index 000000000..f0bb521c6 --- /dev/null +++ b/src/Helpers/CanEvaluateShortCircuit.php @@ -0,0 +1,27 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Helpers; + +use Respect\Validation\Result; +use Respect\Validation\Validator; +use Respect\Validation\Validators\Core\ShortCircuitable; + +trait CanEvaluateShortCircuit +{ + private function evaluateShortCircuitWith(Validator $validator, mixed $input): Result + { + if ($validator instanceof ShortCircuitable) { + return $validator->evaluateShortCircuit($input); + } + + return $validator->evaluate($input); + } +} diff --git a/src/Mixins/AllBuilder.php b/src/Mixins/AllBuilder.php index 430de0f21..0073f247e 100644 --- a/src/Mixins/AllBuilder.php +++ b/src/Mixins/AllBuilder.php @@ -54,8 +54,6 @@ public static function allCallableType(): Chain; public static function allCharset(string $charset, string ...$charsets): Chain; - public static function allCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function allCnh(): Chain; public static function allCnpj(): Chain; @@ -257,6 +255,8 @@ public static function allSatisfies(callable $callback, mixed ...$arguments): Ch public static function allScalarVal(): Chain; + public static function allShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function allSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/AllChain.php b/src/Mixins/AllChain.php index 7e444f1d6..01fe79dfa 100644 --- a/src/Mixins/AllChain.php +++ b/src/Mixins/AllChain.php @@ -54,8 +54,6 @@ public function allCallableType(): Chain; public function allCharset(string $charset, string ...$charsets): Chain; - public function allCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function allCnh(): Chain; public function allCnpj(): Chain; @@ -257,6 +255,8 @@ public function allSatisfies(callable $callback, mixed ...$arguments): Chain; public function allScalarVal(): Chain; + public function allShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function allSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/Builder.php b/src/Mixins/Builder.php index 027360f36..66d62eaa8 100644 --- a/src/Mixins/Builder.php +++ b/src/Mixins/Builder.php @@ -59,8 +59,6 @@ public static function callableType(): Chain; public static function charset(string $charset, string ...$charsets): Chain; - public static function circuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function cnh(): Chain; public static function cnpj(): Chain; @@ -282,6 +280,8 @@ public static function satisfies(callable $callback, mixed ...$arguments): Chain public static function scalarVal(): Chain; + public static function shortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function size(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/Chain.php b/src/Mixins/Chain.php index 81b8c00a4..974ee3115 100644 --- a/src/Mixins/Chain.php +++ b/src/Mixins/Chain.php @@ -61,8 +61,6 @@ public function callableType(): Chain; public function charset(string $charset, string ...$charsets): Chain; - public function circuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function cnh(): Chain; public function cnpj(): Chain; @@ -284,6 +282,8 @@ public function satisfies(callable $callback, mixed ...$arguments): Chain; public function scalarVal(): Chain; + public function shortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function size(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/KeyBuilder.php b/src/Mixins/KeyBuilder.php index a81a5468f..ce2e653ad 100644 --- a/src/Mixins/KeyBuilder.php +++ b/src/Mixins/KeyBuilder.php @@ -56,8 +56,6 @@ public static function keyCallableType(int|string $key): Chain; public static function keyCharset(int|string $key, string $charset, string ...$charsets): Chain; - public static function keyCircuit(int|string $key, Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function keyCnh(int|string $key): Chain; public static function keyCnpj(int|string $key): Chain; @@ -259,6 +257,8 @@ public static function keySatisfies(int|string $key, callable $callback, mixed . public static function keyScalarVal(int|string $key): Chain; + public static function keyShortCircuit(int|string $key, Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function keySize(int|string $key, string $unit, Validator $validator): Chain; diff --git a/src/Mixins/KeyChain.php b/src/Mixins/KeyChain.php index 8f75b636c..33ddb6c82 100644 --- a/src/Mixins/KeyChain.php +++ b/src/Mixins/KeyChain.php @@ -56,8 +56,6 @@ public function keyCallableType(int|string $key): Chain; public function keyCharset(int|string $key, string $charset, string ...$charsets): Chain; - public function keyCircuit(int|string $key, Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function keyCnh(int|string $key): Chain; public function keyCnpj(int|string $key): Chain; @@ -259,6 +257,8 @@ public function keySatisfies(int|string $key, callable $callback, mixed ...$argu public function keyScalarVal(int|string $key): Chain; + public function keyShortCircuit(int|string $key, Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function keySize(int|string $key, string $unit, Validator $validator): Chain; diff --git a/src/Mixins/NotBuilder.php b/src/Mixins/NotBuilder.php index d74c5ea19..d567689ba 100644 --- a/src/Mixins/NotBuilder.php +++ b/src/Mixins/NotBuilder.php @@ -56,8 +56,6 @@ public static function notCallableType(): Chain; public static function notCharset(string $charset, string ...$charsets): Chain; - public static function notCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function notCnh(): Chain; public static function notCnpj(): Chain; @@ -273,6 +271,8 @@ public static function notSatisfies(callable $callback, mixed ...$arguments): Ch public static function notScalarVal(): Chain; + public static function notShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function notSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/NotChain.php b/src/Mixins/NotChain.php index ab846e6a8..7ee90aa61 100644 --- a/src/Mixins/NotChain.php +++ b/src/Mixins/NotChain.php @@ -56,8 +56,6 @@ public function notCallableType(): Chain; public function notCharset(string $charset, string ...$charsets): Chain; - public function notCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function notCnh(): Chain; public function notCnpj(): Chain; @@ -273,6 +271,8 @@ public function notSatisfies(callable $callback, mixed ...$arguments): Chain; public function notScalarVal(): Chain; + public function notShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function notSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/NullOrBuilder.php b/src/Mixins/NullOrBuilder.php index 77abd505e..1a1f8e761 100644 --- a/src/Mixins/NullOrBuilder.php +++ b/src/Mixins/NullOrBuilder.php @@ -56,8 +56,6 @@ public static function nullOrCallableType(): Chain; public static function nullOrCharset(string $charset, string ...$charsets): Chain; - public static function nullOrCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function nullOrCnh(): Chain; public static function nullOrCnpj(): Chain; @@ -275,6 +273,8 @@ public static function nullOrSatisfies(callable $callback, mixed ...$arguments): public static function nullOrScalarVal(): Chain; + public static function nullOrShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function nullOrSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/NullOrChain.php b/src/Mixins/NullOrChain.php index 887bf40f0..39ffd9041 100644 --- a/src/Mixins/NullOrChain.php +++ b/src/Mixins/NullOrChain.php @@ -56,8 +56,6 @@ public function nullOrCallableType(): Chain; public function nullOrCharset(string $charset, string ...$charsets): Chain; - public function nullOrCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function nullOrCnh(): Chain; public function nullOrCnpj(): Chain; @@ -275,6 +273,8 @@ public function nullOrSatisfies(callable $callback, mixed ...$arguments): Chain; public function nullOrScalarVal(): Chain; + public function nullOrShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function nullOrSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/PropertyBuilder.php b/src/Mixins/PropertyBuilder.php index 9f56dcf32..41177c88a 100644 --- a/src/Mixins/PropertyBuilder.php +++ b/src/Mixins/PropertyBuilder.php @@ -56,8 +56,6 @@ public static function propertyCallableType(string $propertyName): Chain; public static function propertyCharset(string $propertyName, string $charset, string ...$charsets): Chain; - public static function propertyCircuit(string $propertyName, Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function propertyCnh(string $propertyName): Chain; public static function propertyCnpj(string $propertyName): Chain; @@ -259,6 +257,8 @@ public static function propertySatisfies(string $propertyName, callable $callbac public static function propertyScalarVal(string $propertyName): Chain; + public static function propertyShortCircuit(string $propertyName, Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function propertySize(string $propertyName, string $unit, Validator $validator): Chain; diff --git a/src/Mixins/PropertyChain.php b/src/Mixins/PropertyChain.php index 902749fc8..ecbd63e6e 100644 --- a/src/Mixins/PropertyChain.php +++ b/src/Mixins/PropertyChain.php @@ -56,8 +56,6 @@ public function propertyCallableType(string $propertyName): Chain; public function propertyCharset(string $propertyName, string $charset, string ...$charsets): Chain; - public function propertyCircuit(string $propertyName, Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function propertyCnh(string $propertyName): Chain; public function propertyCnpj(string $propertyName): Chain; @@ -259,6 +257,8 @@ public function propertySatisfies(string $propertyName, callable $callback, mixe public function propertyScalarVal(string $propertyName): Chain; + public function propertyShortCircuit(string $propertyName, Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function propertySize(string $propertyName, string $unit, Validator $validator): Chain; diff --git a/src/Mixins/UndefOrBuilder.php b/src/Mixins/UndefOrBuilder.php index 2e808586a..1245a659e 100644 --- a/src/Mixins/UndefOrBuilder.php +++ b/src/Mixins/UndefOrBuilder.php @@ -54,8 +54,6 @@ public static function undefOrCallableType(): Chain; public static function undefOrCharset(string $charset, string ...$charsets): Chain; - public static function undefOrCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public static function undefOrCnh(): Chain; public static function undefOrCnpj(): Chain; @@ -273,6 +271,8 @@ public static function undefOrSatisfies(callable $callback, mixed ...$arguments) public static function undefOrScalarVal(): Chain; + public static function undefOrShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public static function undefOrSize(string $unit, Validator $validator): Chain; diff --git a/src/Mixins/UndefOrChain.php b/src/Mixins/UndefOrChain.php index 505107841..f2479a5d8 100644 --- a/src/Mixins/UndefOrChain.php +++ b/src/Mixins/UndefOrChain.php @@ -54,8 +54,6 @@ public function undefOrCallableType(): Chain; public function undefOrCharset(string $charset, string ...$charsets): Chain; - public function undefOrCircuit(Validator $validator1, Validator $validator2, Validator ...$validators): Chain; - public function undefOrCnh(): Chain; public function undefOrCnpj(): Chain; @@ -273,6 +271,8 @@ public function undefOrSatisfies(callable $callback, mixed ...$arguments): Chain public function undefOrScalarVal(): Chain; + public function undefOrShortCircuit(Validator ...$validators): Chain; + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function undefOrSize(string $unit, Validator $validator): Chain; diff --git a/src/ValidatorBuilder.php b/src/ValidatorBuilder.php index 85b3a786c..60f544cbc 100644 --- a/src/ValidatorBuilder.php +++ b/src/ValidatorBuilder.php @@ -17,6 +17,8 @@ use Respect\Validation\Mixins\Builder; use Respect\Validation\Validators\AllOf; use Respect\Validation\Validators\Core\Nameable; +use Respect\Validation\Validators\Core\ShortCircuitable; +use Respect\Validation\Validators\ShortCircuit; use Throwable; use function count; @@ -26,7 +28,7 @@ use function is_string; /** @mixin Builder */ -final readonly class ValidatorBuilder implements Nameable +final readonly class ValidatorBuilder implements Nameable, ShortCircuitable { /** @var array */ private array $validators; @@ -65,6 +67,11 @@ public function evaluate(mixed $input): Result return $validator->evaluate($input); } + public function evaluateShortCircuit(mixed $input): Result + { + return (new ShortCircuit(...$this->validators))->evaluate($input); + } + /** @param array|string|null $template */ public function validate(mixed $input, array|string|null $template = null): ResultQuery { @@ -73,29 +80,19 @@ public function validate(mixed $input, array|string|null $template = null): Resu public function isValid(mixed $input): bool { - return $this->evaluate($input)->hasPassed; + return $this->evaluateShortCircuit($input)->hasPassed; } /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ - public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void + public function check(mixed $input, array|string|Throwable|callable|null $template = null): void { - $result = $this->evaluate($input); - if ($result->hasPassed) { - return; - } - - if ($template instanceof Throwable) { - throw $template; - } - - $resultQuery = $this->toResultQuery($result, is_callable($template) ? null : $template); - - $exception = new ValidationException($resultQuery->getMessage(), $resultQuery, ...$this->ignoredBacktracePaths); - if (is_callable($template)) { - throw $template($exception); - } + $this->throwOnFailure($this->evaluateShortCircuit($input), $template); + } - throw $exception; + /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ + public function assert(mixed $input, array|string|Throwable|callable|null $template = null): void + { + $this->throwOnFailure($this->evaluate($input), $template); } public function with(Validator $validator, Validator ...$validators): self @@ -131,6 +128,27 @@ private function toResultQuery(Result $result, array|string|null $template): Res ); } + /** @param array|callable(ValidationException): Throwable|string|Throwable|null $template */ + private function throwOnFailure(Result $result, array|callable|Throwable|string|null $template): void + { + if ($result->hasPassed) { + return; + } + + if ($template instanceof Throwable) { + throw $template; + } + + $resultQuery = $this->toResultQuery($result, is_callable($template) ? null : $template); + + $exception = new ValidationException($resultQuery->getMessage(), $resultQuery, ...$this->ignoredBacktracePaths); + if (is_callable($template)) { + throw $template($exception); + } + + throw $exception; + } + /** @param array $arguments */ public static function __callStatic(string $ruleName, array $arguments): self { diff --git a/src/Validators/All.php b/src/Validators/All.php index 1c799dfda..613ad6cf3 100644 --- a/src/Validators/All.php +++ b/src/Validators/All.php @@ -15,14 +15,41 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; +use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; +use Respect\Validation\Validators\Core\ShortCircuitable; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template('Every item in', 'Every item in')] -final class All extends FilteredArray +final class All extends FilteredArray implements ShortCircuitable { + use CanEvaluateShortCircuit; + + public function evaluateShortCircuit(mixed $input): Result + { + $iterableResult = (new IterableType())->evaluate($input); + if (!$iterableResult->hasPassed) { + return $iterableResult->withIdFrom($this); + } + + $result = null; + foreach ($input as $key => $value) { + $result = $this->evaluateShortCircuitWith($this->validator, $value); + if (!$result->hasPassed) { + return $result->withPath(new Path($key)); + } + } + + if ($result === null) { + return Result::passed($input, $this)->asIndeterminate(); + } + + return Result::passed($input, $this)->asAdjacentOf($result, 'all'); + } + /** @param non-empty-array $input */ protected function evaluateArray(array $input): Result { diff --git a/src/Validators/AllOf.php b/src/Validators/AllOf.php index 5ee7ca546..e6c9765ea 100644 --- a/src/Validators/AllOf.php +++ b/src/Validators/AllOf.php @@ -15,10 +15,12 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\ShortCircuitable; use function array_filter; use function array_map; @@ -36,8 +38,10 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] -final class AllOf extends Composite +final class AllOf extends Composite implements ShortCircuitable { + use CanEvaluateShortCircuit; + public const string TEMPLATE_ALL = '__all__'; public const string TEMPLATE_SOME = '__some__'; @@ -53,4 +57,18 @@ public function evaluate(mixed $input): Result return Result::of($valid, $input, $this, [], $template)->withChildren(...$children); } + + public function evaluateShortCircuit(mixed $input): Result + { + $children = []; + foreach ($this->validators as $validator) { + $result = $this->evaluateShortCircuitWith($validator, $input); + $children[] = $result; + if (!$result->hasPassed) { + return $result; + } + } + + return Result::passed($input, $this)->withChildren(...$children); + } } diff --git a/src/Validators/AnyOf.php b/src/Validators/AnyOf.php index 5518e8045..61ac0a3bc 100644 --- a/src/Validators/AnyOf.php +++ b/src/Validators/AnyOf.php @@ -15,10 +15,12 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\ShortCircuitable; use function array_map; use function array_reduce; @@ -28,8 +30,10 @@ '{{subject}} must pass at least one of the rules', '{{subject}} must pass at least one of the rules', )] -final class AnyOf extends Composite +final class AnyOf extends Composite implements ShortCircuitable { + use CanEvaluateShortCircuit; + public function evaluate(mixed $input): Result { $children = array_map(static fn(Validator $validator) => $validator->evaluate($input), $this->validators); @@ -41,4 +45,18 @@ public function evaluate(mixed $input): Result return Result::of($valid, $input, $this)->withChildren(...$children); } + + public function evaluateShortCircuit(mixed $input): Result + { + $children = []; + foreach ($this->validators as $validator) { + $result = $this->evaluateShortCircuitWith($validator, $input); + $children[] = $result; + if ($result->hasPassed) { + return Result::passed($input, $this)->withChildren(...$children); + } + } + + return Result::failed($input, $this)->withChildren(...$children); + } } diff --git a/src/Validators/Core/ShortCircuitable.php b/src/Validators/Core/ShortCircuitable.php new file mode 100644 index 000000000..4f18e246d --- /dev/null +++ b/src/Validators/Core/ShortCircuitable.php @@ -0,0 +1,19 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Validators\Core; + +use Respect\Validation\Result; +use Respect\Validation\Validator; + +interface ShortCircuitable extends Validator +{ + public function evaluateShortCircuit(mixed $input): Result; +} diff --git a/src/Validators/Domain.php b/src/Validators/Domain.php index 46644ce19..1fe67f28f 100644 --- a/src/Validators/Domain.php +++ b/src/Validators/Domain.php @@ -63,9 +63,9 @@ public function evaluate(mixed $input): Result return Result::of($this->partsRule->evaluate($parts)->hasPassed, $input, $this); } - private function createGenericRule(): Circuit + private function createGenericRule(): ShortCircuit { - return new Circuit( + return new ShortCircuit( new StringType(), new Not(new Spaced()), new Contains('.'), @@ -79,13 +79,13 @@ private function createTldRule(bool $realTldCheck): Validator return new Tld(); } - return new Circuit(new Not(new StartsWith('-')), new Length(new GreaterThanOrEqual(2))); + return new ShortCircuit(new Not(new StartsWith('-')), new Length(new GreaterThanOrEqual(2))); } private function createPartsRule(): Validator { return new Each( - new Circuit( + new ShortCircuit( new Alnum('-'), new Not(new StartsWith('-')), new AnyOf( diff --git a/src/Validators/Each.php b/src/Validators/Each.php index 1c9faecc9..4673e1f43 100644 --- a/src/Validators/Each.php +++ b/src/Validators/Each.php @@ -18,10 +18,12 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Path; use Respect\Validation\Result; use Respect\Validation\Validators\Core\FilteredArray; +use Respect\Validation\Validators\Core\ShortCircuitable; use function array_reduce; @@ -30,8 +32,32 @@ 'Each item in {{subject}} must be valid', 'Each item in {{subject}} must be invalid', )] -final class Each extends FilteredArray +final class Each extends FilteredArray implements ShortCircuitable { + use CanEvaluateShortCircuit; + + public function evaluateShortCircuit(mixed $input): Result + { + $iterableResult = (new IterableType())->evaluate($input); + if (!$iterableResult->hasPassed) { + return $iterableResult->withIdFrom($this); + } + + $children = []; + foreach ($input as $key => $value) { + $result = $this->evaluateShortCircuitWith($this->validator, $value) + ->withPath(new Path($key)) + ->withPrecedentName(false); + if (!$result->hasPassed) { + return $result; + } + + $children[] = $result; + } + + return Result::passed($input, $this)->withChildren(...$children); + } + /** @param array $input */ protected function evaluateArray(array $input): Result { diff --git a/src/Validators/KeySet.php b/src/Validators/KeySet.php index 2c7a8efae..68373cab9 100644 --- a/src/Validators/KeySet.php +++ b/src/Validators/KeySet.php @@ -16,12 +16,14 @@ use Attribute; use Respect\Validation\Exceptions\InvalidValidatorException; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\ValidatorBuilder; use Respect\Validation\Validators\Core\KeyRelated; use Respect\Validation\Validators\Core\Reducer; +use Respect\Validation\Validators\Core\ShortCircuitable; use function array_diff; use function array_filter; @@ -50,8 +52,10 @@ '{{subject}} contains no missing keys', self::TEMPLATE_MISSING_KEYS, )] -final readonly class KeySet implements Validator +final readonly class KeySet implements ShortCircuitable { + use CanEvaluateShortCircuit; + public const string TEMPLATE_BOTH = '__both__'; public const string TEMPLATE_EXTRA_KEYS = '__extra_keys__'; public const string TEMPLATE_MISSING_KEYS = '__missing_keys__'; @@ -96,6 +100,38 @@ public function evaluate(mixed $input): Result ->withChildren(...($keysResult->children === [] ? [$keysResult] : $keysResult->children)); } + public function evaluateShortCircuit(mixed $input): Result + { + $arrayResult = (new ArrayType())->evaluate($input); + if (!$arrayResult->hasPassed) { + return $arrayResult; + } + + $children = []; + foreach ($this->validators as $validator) { + $result = $this->evaluateShortCircuitWith($validator, $input); + if (!$result->hasPassed) { + return $result; + } + + $children[] = $result; + } + + $extraKeys = array_slice(array_diff(array_keys($input), $this->allKeys), 0, self::MAX_DIFF_KEYS); + foreach ($extraKeys as $key) { + $result = (new Not(new KeyExists($key)))->evaluate($input); + if (!$result->hasPassed) { + return $result; + } + + $children[] = $result; + } + + $template = $this->getTemplateFromKeys(array_keys($input)); + + return Result::passed($input, $this, [], $template)->withChildren(...$children); + } + /** * @param array $validators * diff --git a/src/Validators/NoneOf.php b/src/Validators/NoneOf.php index f7fb46d5c..c51ce5d33 100644 --- a/src/Validators/NoneOf.php +++ b/src/Validators/NoneOf.php @@ -15,9 +15,11 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\ShortCircuitable; use function count; @@ -32,8 +34,10 @@ '{{subject}} must pass all the rules', self::TEMPLATE_ALL, )] -final class NoneOf extends Composite +final class NoneOf extends Composite implements ShortCircuitable { + use CanEvaluateShortCircuit; + public const string TEMPLATE_ALL = '__all__'; public const string TEMPLATE_SOME = '__some__'; @@ -59,4 +63,19 @@ public function evaluate(mixed $input): Result count($children) === $failedCount ? self::TEMPLATE_ALL : self::TEMPLATE_SOME, )->withChildren(...$children); } + + public function evaluateShortCircuit(mixed $input): Result + { + $children = []; + foreach ($this->validators as $validator) { + $result = $this->evaluateShortCircuitWith($validator, $input)->withToggledModeAndValidation(); + if (!$result->hasPassed) { + return $result; + } + + $children[] = $result; + } + + return Result::passed($input, $this)->withChildren(...$children); + } } diff --git a/src/Validators/OneOf.php b/src/Validators/OneOf.php index b3b5f3eee..d5489e5ba 100644 --- a/src/Validators/OneOf.php +++ b/src/Validators/OneOf.php @@ -16,10 +16,12 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Message\Template; use Respect\Validation\Result; use Respect\Validation\Validator; use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validators\Core\ShortCircuitable; use function array_filter; use function array_map; @@ -38,8 +40,10 @@ '{{subject}} must pass only one of the rules', self::TEMPLATE_MORE_THAN_ONE, )] -final class OneOf extends Composite +final class OneOf extends Composite implements ShortCircuitable { + use CanEvaluateShortCircuit; + public const string TEMPLATE_NONE = '__none__'; public const string TEMPLATE_MORE_THAN_ONE = '__more_than_one__'; @@ -66,4 +70,24 @@ public function evaluate(mixed $input): Result return Result::of($valid, $input, $this, [], $template)->withChildren(...$children); } + + public function evaluateShortCircuit(mixed $input): Result + { + $children = []; + $passedCount = 0; + foreach ($this->validators as $validator) { + $result = $this->evaluateShortCircuitWith($validator, $input); + $children[] = $result; + if ($result->hasPassed) { + $passedCount++; + } + + if ($passedCount > 1) { + return Result::failed($input, $this, [], self::TEMPLATE_MORE_THAN_ONE)->withChildren(...$children); + } + } + + return Result::of($passedCount === 1, $input, $this, [], self::TEMPLATE_NONE) + ->withChildren(...$children); + } } diff --git a/src/Validators/Circuit.php b/src/Validators/ShortCircuit.php similarity index 58% rename from src/Validators/Circuit.php rename to src/Validators/ShortCircuit.php index 8c0dd45dd..7be4cd1ce 100644 --- a/src/Validators/Circuit.php +++ b/src/Validators/ShortCircuit.php @@ -12,16 +12,27 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\CanEvaluateShortCircuit; use Respect\Validation\Result; -use Respect\Validation\Validators\Core\Composite; +use Respect\Validation\Validator; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] -final class Circuit extends Composite +final readonly class ShortCircuit implements Validator { + use CanEvaluateShortCircuit; + + /** @var non-empty-array */ + private array $validators; + + public function __construct(Validator ...$validators) + { + $this->validators = $validators === [] ? [new AlwaysValid()] : $validators; + } + public function evaluate(mixed $input): Result { foreach ($this->validators as $validator) { - $result = $validator->evaluate($input); + $result = $this->evaluateShortCircuitWith($validator, $input); if (!$result->hasPassed) { return $result; } diff --git a/src/Validators/Tld.php b/src/Validators/Tld.php index c0ef572ae..9a6a81159 100644 --- a/src/Validators/Tld.php +++ b/src/Validators/Tld.php @@ -23,7 +23,7 @@ final class Tld extends Envelope { public function __construct() { - parent::__construct(new Circuit( + parent::__construct(new ShortCircuit( new StringType(), new After('mb_strtoupper', new In(DataLoader::load('domain/tld.php'))), )); diff --git a/src/Validators/Url.php b/src/Validators/Url.php index 354e38f29..868eb49f7 100644 --- a/src/Validators/Url.php +++ b/src/Validators/Url.php @@ -34,17 +34,17 @@ public function __construct() { $this->validator = new After( 'parse_url', - new Circuit( + new ShortCircuit( new ArrayType(), new OneOf( - new Circuit( + new ShortCircuit( new Key('scheme', new In(['http', 'https', 'ftp', 'telnet', 'gopher', 'ldap'])), new Key('host', new OneOf( new Domain(), new After([self::class, 'formatIp'], new Ip()), )), ), - new Circuit( + new ShortCircuit( new Key('scheme', new Equals('mailto')), new Key('path', new Email()), ), diff --git a/tests/benchmark/CompositeValidatorsBench.php b/tests/benchmark/CompositeValidatorsBench.php new file mode 100644 index 000000000..6bdeaa666 --- /dev/null +++ b/tests/benchmark/CompositeValidatorsBench.php @@ -0,0 +1,121 @@ + + * SPDX-FileContributor: Henrique Moody + */ + +declare(strict_types=1); + +namespace Respect\Validation\Benchmarks; + +use Generator; +use PhpBench\Attributes as Bench; +use Respect\Validation\Validator; +use Respect\Validation\ValidatorBuilder; +use Respect\Validation\Validators\Alnum; +use Respect\Validation\Validators\Alpha; +use Respect\Validation\Validators\BoolType; +use Respect\Validation\Validators\Digit; +use Respect\Validation\Validators\Even; +use Respect\Validation\Validators\FloatType; +use Respect\Validation\Validators\IntType; +use Respect\Validation\Validators\Negative; +use Respect\Validation\Validators\Positive; +use Respect\Validation\Validators\StringType; + +use function count; +use function range; + +final class CompositeValidatorsBench +{ + /** @param array{string, array} $params */ + #[Bench\ParamProviders(['provideGroupedValidatorBuilder'])] + #[Bench\Iterations(5)] + #[Bench\Revs(50)] + #[Bench\Warmup(1)] + #[Bench\Subject] + public function isValidGrouped(array $params): void + { + ValidatorBuilder::__callStatic(...$params)->isValid(42); + } + + /** @param array{string, array} $params */ + #[Bench\ParamProviders(['provideArrayBasedValidatorBuilder'])] + #[Bench\Iterations(5)] + #[Bench\Revs(50)] + #[Bench\Warmup(1)] + #[Bench\Subject] + public function isValidArrayBased(array $params): void + { + [$validator, $validators] = $params; + ValidatorBuilder::__callStatic($validator, $validators)->isValid(range(1, count($validators))); + } + + #[Bench\ParamProviders(['provideInvalidDomain'])] + #[Bench\Iterations(5)] + #[Bench\Revs(50)] + #[Bench\Warmup(1)] + #[Bench\Subject] + public function isValidDomain(mixed $input): void + { + ValidatorBuilder::domain()->isValid($input); + } + + public function provideGroupedValidatorBuilder(): Generator + { + yield 'allOf(10)' => ['allOf', $this->buildValidators(10)]; + yield 'oneOf(10)' => ['oneOf', $this->buildValidators(10)]; + yield 'anyOf(10)' => ['anyOf', $this->buildValidators(10)]; + yield 'noneOf(10)' => ['noneOf', $this->buildValidators(10)]; + yield 'allOf(100)' => ['allOf', $this->buildValidators(100)]; + yield 'oneOf(100)' => ['oneOf', $this->buildValidators(100)]; + yield 'anyOf(100)' => ['anyOf', $this->buildValidators(100)]; + yield 'noneOf(100)' => ['noneOf', $this->buildValidators(100)]; + } + + public function provideArrayBasedValidatorBuilder(): Generator + { + yield 'all(10)' => ['all', $this->buildValidators(10)]; + yield 'each(10)' => ['each', $this->buildValidators(10)]; + yield 'all(100)' => ['all', $this->buildValidators(100)]; + yield 'each(100)' => ['each', $this->buildValidators(100)]; + } + + public function provideInvalidDomain(): Generator + { + yield 'no dots' => ['no dots']; + yield 'starts with "-"' => ['-example-invalid.com']; + yield 'ends with "-"' => ['example.invalid-.com']; + yield 'double "--"' => ['xn--bcher--kva.ch']; + } + + /** @return array */ + private function buildValidators(int $count): array + { + $validators = []; + for ($i = 0; $i < $count; $i++) { + $validators[] = $this->makeValidator($i); + } + + return $validators; + } + + private function makeValidator(int $index): Validator + { + return match ($index % 10) { + 0 => new IntType(), + 1 => new Positive(), + 2 => new Negative(), + 3 => new Even(), + 4 => new FloatType(), + 5 => new StringType(), + 6 => new Alpha(), + 7 => new Alnum(), + 8 => new Digit(), + default => new BoolType(), + }; + } +} diff --git a/tests/feature/Validators/AllOfTest.php b/tests/feature/Validators/AllOfTest.php index fb0751cb6..d3217f9a9 100644 --- a/tests/feature/Validators/AllOfTest.php +++ b/tests/feature/Validators/AllOfTest.php @@ -123,3 +123,24 @@ 'uppercase' => 'Template for "uppercase"', ]), )); + +test('short-circuit: first validator fails', catchAll( + fn() => v::shortCircuit(v::intType(), v::negative(), v::greaterThan(10))->assert('string'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"string" must be an integer') + ->and($fullMessage)->toBe('- "string" must be an integer') + ->and($messages)->toBe(['intType' => '"string" must be an integer']), +)); + +test('short-circuit: second validator fails', catchAll( + fn() => v::shortCircuit(v::intType(), v::negative(), v::greaterThan(10))->assert(5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('5 must be a negative number') + ->and($fullMessage)->toBe('- 5 must be a negative number') + ->and($messages)->toBe(['negative' => '5 must be a negative number']), +)); + +test('short-circuit: all validators pass', function (): void { + $validator = v::shortCircuit(v::intType(), v::negative(), v::greaterThan(-10)); + expect($validator->isValid(-5))->toBeTrue(); +}); diff --git a/tests/feature/Validators/AnyOfTest.php b/tests/feature/Validators/AnyOfTest.php index c427950d4..9f5e28702 100644 --- a/tests/feature/Validators/AnyOfTest.php +++ b/tests/feature/Validators/AnyOfTest.php @@ -55,3 +55,59 @@ 'negative' => '-1 must not be a negative number', ]), )); + +test('short-circuit: first validator passes', catchAll( + fn() => v::shortCircuit(v::intType(), v::negative(), v::greaterThan(10))->assert('string'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"string" must be an integer') + ->and($fullMessage)->toBe('- "string" must be an integer') + ->and($messages)->toBe(['intType' => '"string" must be an integer']), +)); + +test('short-circuit: second validator passes', catchAll( + fn() => v::shortCircuit(v::intType(), v::negative(), v::greaterThan(10))->assert(5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('5 must be a negative number') + ->and($fullMessage)->toBe('- 5 must be a negative number') + ->and($messages)->toBe(['negative' => '5 must be a negative number']), +)); + +test('short-circuit: all validators pass', catchAll( + fn() => v::shortCircuit(v::intType(), v::negative())->assert(5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('5 must be a negative number') + ->and($fullMessage)->toBe('- 5 must be a negative number') + ->and($messages)->toBe(['negative' => '5 must be a negative number']), +)); + +test('short-circuit: AnyOf wrapped by ShortCircuit stops on first validator fail', catchAll( + fn() => v::shortCircuit(v::alwaysInvalid(), v::anyOf(v::stringType(), v::intType()))->assert('hello'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"hello" must be valid') + ->and($fullMessage)->toBe('- "hello" must be valid') + ->and($messages)->toBe(['alwaysInvalid' => '"hello" must be valid']), +)); + +test('short-circuit: AnyOf wrapped by ShortCircuit passes on first match of AnyOf', catchAll( + fn() => v::shortCircuit(v::stringType(), v::anyOf(v::intType(), v::negative()))->assert(5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('5 must be a string') + ->and($fullMessage)->toBe('- 5 must be a string') + ->and($messages)->toBe(['stringType' => '5 must be a string']), +)); + +test('short-circuit: AnyOf wrapped by ShortCircuit passes on second match of AnyOf', catchAll( + fn() => v::shortCircuit(v::stringType(), v::anyOf(v::intType(), v::negative()))->assert(-5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('-5 must be a string') + ->and($fullMessage)->toBe('- -5 must be a string') + ->and($messages)->toBe(['stringType' => '-5 must be a string']), +)); + +test('short-circuit: AnyOf wrapped by ShortCircuit fails when AnyOf all fail', catchAll( + fn() => v::shortCircuit(v::stringType(), v::anyOf(v::intType(), v::negative()))->assert(3.14), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('3.14 must be a string') + ->and($fullMessage)->toBe('- 3.14 must be a string') + ->and($messages)->toBe(['stringType' => '3.14 must be a string']), +)); diff --git a/tests/feature/Validators/EachTest.php b/tests/feature/Validators/EachTest.php index d700482a1..f6e353bc8 100644 --- a/tests/feature/Validators/EachTest.php +++ b/tests/feature/Validators/EachTest.php @@ -301,3 +301,37 @@ ], ]), )); + +test('short-circuit: first item fails', catchAll( + fn() => v::shortCircuit(v::each(v::intType()))->assert(['a', 2, 3]), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.0` must be an integer') + ->and($fullMessage)->toBe('- `.0` must be an integer') + ->and($messages)->toBe([0 => '`.0` must be an integer']), +)); + +test('short-circuit: second item fails', catchAll( + fn() => v::shortCircuit(v::each(v::intType()))->assert([1, 2.5, 3]), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.1` must be an integer') + ->and($fullMessage)->toBe('- `.1` must be an integer') + ->and($messages)->toBe([1 => '`.1` must be an integer']), +)); + +test('short-circuit: all items pass', function (): void { + $validator = v::shortCircuit(v::each(v::intType())); + expect($validator->isValid([1, 2, 3]))->toBeTrue(); +}); + +test('short-circuit: empty array', function (): void { + $validator = v::shortCircuit(v::each(v::intType())); + expect($validator->isValid([]))->toBeTrue(); +}); + +test('short-circuit: non-iterable input', catchAll( + fn() => v::shortCircuit(v::each(v::intType()))->assert(null), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`null` must be iterable') + ->and($fullMessage)->toBe('- `null` must be iterable') + ->and($messages)->toBe(['each' => '`null` must be iterable']), +)); diff --git a/tests/feature/Validators/KeySetTest.php b/tests/feature/Validators/KeySetTest.php index 2cf0b6629..27ed99cfb 100644 --- a/tests/feature/Validators/KeySetTest.php +++ b/tests/feature/Validators/KeySetTest.php @@ -227,3 +227,134 @@ 'baz' => '`.baz` must be present', ]), )); + +test('short-circuit / first key fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('foo', v::intType()), + v::key('bar', v::intType()), + ), + ) + ->assert(['foo' => 'string', 'bar' => 'string']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.foo` must be an integer') + ->and($fullMessage)->toBe('- `.foo` must be an integer') + ->and($messages)->toBe(['foo' => '`.foo` must be an integer']), +)); + +test('short-circuit / extra key', catchAll( + fn() => v::shortCircuit(v::keySet(v::keyExists('foo')))->assert(['foo' => 42, 'bar' => 'extra']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.bar` must not be present') + ->and($fullMessage)->toBe('- `.bar` must not be present') + ->and($messages)->toBe(['bar' => '`.bar` must not be present']), +)); + +test('short-circuit / not an array', catchAll( + fn() => v::shortCircuit(v::keySet(v::keyExists('foo')))->assert('not-an-array'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"not-an-array" must be an array') + ->and($fullMessage)->toBe('- "not-an-array" must be an array') + ->and($messages)->toBe(['arrayType' => '"not-an-array" must be an array']), +)); + +test('short-circuit / second key fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('foo', v::intType()), + v::key('bar', v::intType()), + v::key('baz', v::intType()), + ), + ) + ->assert(['foo' => 1, 'bar' => 'string', 'baz' => 3]), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.bar` must be an integer') + ->and($fullMessage)->toBe('- `.bar` must be an integer') + ->and($messages)->toBe(['bar' => '`.bar` must be an integer']), +)); + +test('short-circuit / third key fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('foo', v::intType()), + v::key('bar', v::intType()), + v::key('baz', v::intType()), + ), + ) + ->assert(['foo' => 1, 'bar' => 2, 'baz' => 'string']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.baz` must be an integer') + ->and($fullMessage)->toBe('- `.baz` must be an integer') + ->and($messages)->toBe(['baz' => '`.baz` must be an integer']), +)); + +test('short-circuit / extra key before third key', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('foo', v::intType()), + v::key('bar', v::intType()), + ), + ) + ->assert(['foo' => 1, 'bar' => 2, 'baz' => 'extra']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.baz` must not be present') + ->and($fullMessage)->toBe('- `.baz` must not be present') + ->and($messages)->toBe(['baz' => '`.baz` must not be present']), +)); + +test('short-circuit / first extra key fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::keyExists('foo'), + v::keyExists('bar'), + ), + ) + ->assert(['foo' => 1, 'bar' => 2, 'extra1' => 'value', 'extra2' => 'value']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.extra1` must not be present') + ->and($fullMessage)->toBe('- `.extra1` must not be present') + ->and($messages)->toBe(['extra1' => '`.extra1` must not be present']), +)); + +test('short-circuit / missing key before extra keys', catchAll( + fn() => v::shortCircuit(v::keySet(v::keyExists('foo'), v::keyExists('bar')))->assert(['foo' => 1, 'extra' => 'value']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.bar` must be present') + ->and($fullMessage)->toBe('- `.bar` must be present') + ->and($messages)->toBe(['bar' => '`.bar` must be present']), +)); + +test('short-circuit / nested KeySet fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('user', v::keySet( + v::key('name', v::stringType()), + v::key('email', v::email()), + )), + ), + ) + ->assert([ + 'user' => [ + 'name' => 'John Doe', + 'email' => 'invalid-email', + ], + ]), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.user.email` must be a valid email address') + ->and($fullMessage)->toBe('- `.user.email` must be a valid email address') + ->and($messages)->toBe(['email' => '`.user.email` must be a valid email address']), +)); + +test('short-circuit / with keyOptional that fails', catchAll( + fn() => v::shortCircuit( + v::keySet( + v::key('foo', v::stringType()), + v::keyOptional('bar', v::intType()), + ), + ) + ->assert(['foo' => 1, 'bar' => 'string']), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.foo` must be a string') + ->and($fullMessage)->toBe('- `.foo` must be a string') + ->and($messages)->toBe(['foo' => '`.foo` must be a string']), +)); diff --git a/tests/feature/Validators/NoneOfTest.php b/tests/feature/Validators/NoneOfTest.php index d1d4ac380..97a5b5996 100644 --- a/tests/feature/Validators/NoneOfTest.php +++ b/tests/feature/Validators/NoneOfTest.php @@ -65,3 +65,27 @@ 'negative' => '"string" must be a negative number', ]), )); + +test('short-circuit: first validator passes (should fail)', catchAll( + fn() => v::shortCircuit(v::noneOf(v::intType(), v::negative(), v::greaterThan(10)))->assert(5), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('5 must not be an integer') + ->and($fullMessage)->toBe('- 5 must not be an integer') + ->and($messages)->toBe(['intType' => '5 must not be an integer']), +)); + +test('short-circuit: second validator passes (should fail)', catchAll( + fn() => v::shortCircuit(v::noneOf(v::intType(), v::negative(), v::greaterThan(10)))->assert('-5'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"-5" must not be a negative number') + ->and($fullMessage)->toBe('- "-5" must not be a negative number') + ->and($messages)->toBe(['negative' => '"-5" must not be a negative number']), +)); + +test('short-circuit: all validators fail (should pass)', catchAll( + fn() => v::shortCircuit(v::noneOf(v::intType(), v::negative()))->assert('-1'), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('"-1" must not be a negative number') + ->and($fullMessage)->toBe('- "-1" must not be a negative number') + ->and($messages)->toBe(['negative' => '"-1" must not be a negative number']), +)); diff --git a/tests/feature/Validators/CircuitTest.php b/tests/feature/Validators/ShortCircuitTest.php similarity index 65% rename from tests/feature/Validators/CircuitTest.php rename to tests/feature/Validators/ShortCircuitTest.php index cfdad09bc..ff2d195f4 100644 --- a/tests/feature/Validators/CircuitTest.php +++ b/tests/feature/Validators/ShortCircuitTest.php @@ -9,15 +9,23 @@ declare(strict_types=1); test('Default', catchAll( - fn() => v::circuit(v::alwaysValid(), v::trueVal())->assert(false), + fn() => v::shortCircuit(v::alwaysValid(), v::trueVal())->assert(false), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('`false` must evaluate to `true`') ->and($fullMessage)->toBe('- `false` must evaluate to `true`') ->and($messages)->toBe(['trueVal' => '`false` must evaluate to `true`']), )); +test('With recursive', catchAll( + fn() => v::shortCircuit(v::each(v::intType()))->assert([1, 2, '3', 4]), + fn(string $message, string $fullMessage, array $messages) => expect() + ->and($message)->toBe('`.2` must be an integer') + ->and($fullMessage)->toBe('- `.2` must be an integer') + ->and($messages)->toBe([2 => '`.2` must be an integer']), +)); + test('Inverted', catchAll( - fn() => v::not(v::circuit(v::alwaysValid(), v::trueVal()))->assert(true), + fn() => v::not(v::shortCircuit(v::alwaysValid(), v::trueVal()))->assert(true), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('`true` must not evaluate to `true`') ->and($fullMessage)->toBe('- `true` must not evaluate to `true`') @@ -25,7 +33,7 @@ )); test('Default with inverted failing rule', catchAll( - fn() => v::circuit(v::alwaysValid(), v::not(v::trueVal()))->assert(true), + fn() => v::shortCircuit(v::alwaysValid(), v::not(v::trueVal()))->assert(true), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('`true` must not evaluate to `true`') ->and($fullMessage)->toBe('- `true` must not evaluate to `true`') @@ -33,7 +41,7 @@ )); test('With wrapped name, default', catchAll( - fn() => v::named('Wrapper', v::circuit(v::alwaysValid(), v::named('Wrapped', v::trueVal())))->assert(false), + fn() => v::named('Wrapper', v::shortCircuit(v::alwaysValid(), v::named('Wrapped', v::trueVal())))->assert(false), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('Wrapped must evaluate to `true`') ->and($fullMessage)->toBe('- Wrapped must evaluate to `true`') @@ -41,7 +49,7 @@ )); test('With wrapper name, default', catchAll( - fn() => v::named('Wrapper', v::circuit(v::alwaysValid(), v::trueVal()))->assert(false), + fn() => v::named('Wrapper', v::shortCircuit(v::alwaysValid(), v::trueVal()))->assert(false), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('Wrapper must evaluate to `true`') ->and($fullMessage)->toBe('- Wrapper must evaluate to `true`') @@ -49,7 +57,7 @@ )); test('With the name set in the wrapped rule of an inverted failing rule', catchAll( - fn() => v::named('Wrapper', v::circuit(v::alwaysValid(), v::named('Not', v::not(v::named('Wrapped', v::trueVal())))))->assert(true), + fn() => v::named('Wrapper', v::shortCircuit(v::alwaysValid(), v::named('Not', v::not(v::named('Wrapped', v::trueVal())))))->assert(true), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('Wrapped must not evaluate to `true`') ->and($fullMessage)->toBe('- Wrapped must not evaluate to `true`') @@ -57,15 +65,15 @@ )); test('With the name set in an inverted failing rule', catchAll( - fn() => v::named('Wrapper', v::circuit(v::alwaysValid(), v::named('Not', v::not(v::trueVal()))))->assert(true), + fn() => v::named('Wrapper', v::shortCircuit(v::alwaysValid(), v::named('Not', v::not(v::trueVal()))))->assert(true), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('Not must not evaluate to `true`') ->and($fullMessage)->toBe('- Not must not evaluate to `true`') ->and($messages)->toBe(['notTrueVal' => 'Not must not evaluate to `true`']), )); -test('With the name set in the "circuit" that has an inverted failing rule', catchAll( - fn() => v::named('Wrapper', v::circuit(v::alwaysValid(), v::not(v::trueVal())))->assert(true), +test('With the name set in the "shortCircuit" that has an inverted failing rule', catchAll( + fn() => v::named('Wrapper', v::shortCircuit(v::alwaysValid(), v::not(v::trueVal())))->assert(true), fn(string $message, string $fullMessage, array $messages) => expect() ->and($message)->toBe('Wrapper must not evaluate to `true`') ->and($fullMessage)->toBe('- Wrapper must not evaluate to `true`') @@ -73,25 +81,25 @@ )); test('With template', catchAll( - fn() => v::templated('Circuit cool cats cunningly continuous cookies', v::circuit(v::alwaysValid(), v::trueVal())) + fn() => v::templated('ShortCircuit cool cats cunningly continuous cookies', v::shortCircuit(v::alwaysValid(), v::trueVal())) ->assert(false), fn(string $message, string $fullMessage, array $messages) => expect() - ->and($message)->toBe('Circuit cool cats cunningly continuous cookies') - ->and($fullMessage)->toBe('- Circuit cool cats cunningly continuous cookies') - ->and($messages)->toBe(['trueVal' => 'Circuit cool cats cunningly continuous cookies']), + ->and($message)->toBe('ShortCircuit cool cats cunningly continuous cookies') + ->and($fullMessage)->toBe('- ShortCircuit cool cats cunningly continuous cookies') + ->and($messages)->toBe(['trueVal' => 'ShortCircuit cool cats cunningly continuous cookies']), )); test('With multiple templates', catchAll( - fn() => v::circuit(v::alwaysValid(), v::trueVal()) - ->assert(false, ['trueVal' => 'Clever clowns craft circuit clever clocks']), + fn() => v::shortCircuit(v::alwaysValid(), v::trueVal()) + ->assert(false, ['trueVal' => 'Clever clowns craft shortCircuit clever clocks']), fn(string $message, string $fullMessage, array $messages) => expect() - ->and($message)->toBe('Clever clowns craft circuit clever clocks') - ->and($fullMessage)->toBe('- Clever clowns craft circuit clever clocks') - ->and($messages)->toBe(['trueVal' => 'Clever clowns craft circuit clever clocks']), + ->and($message)->toBe('Clever clowns craft shortCircuit clever clocks') + ->and($fullMessage)->toBe('- Clever clowns craft shortCircuit clever clocks') + ->and($messages)->toBe(['trueVal' => 'Clever clowns craft shortCircuit clever clocks']), )); test('Real example', catchAll( - fn() => v::circuit( + fn() => v::shortCircuit( v::key('countyCode', v::countryCode()), v::factory( fn($input) => v::key('subdivisionCode', v::subdivisionCode($input['countyCode'])), diff --git a/tests/src/SmokeTestProvider.php b/tests/src/SmokeTestProvider.php index d5e270576..ccbfa6ccd 100644 --- a/tests/src/SmokeTestProvider.php +++ b/tests/src/SmokeTestProvider.php @@ -44,7 +44,6 @@ public static function provideValidatorInput(): Generator yield 'Bsn' => [new vs\Bsn(), '612890053']; yield 'CallableType' => [new vs\CallableType(), [static::class, 'callableTarget']]; yield 'Charset' => [new vs\Charset('UTF-8'), 'example']; - yield 'Circuit' => [new vs\Circuit(new vs\IntVal(), new vs\GreaterThan(0)), 5]; yield 'Cnh' => [new vs\Cnh(), '02650306461']; yield 'Cnpj' => [new vs\Cnpj(), '11444777000161']; yield 'Consonant' => [new vs\Consonant(), 'bcdf']; @@ -75,6 +74,7 @@ public static function provideValidatorInput(): Generator yield 'Exists' => [new vs\Exists(), 'tests/fixtures/valid-image.png']; yield 'Extension' => [new vs\Extension('png'), 'image.png']; yield 'Factor' => [new vs\Factor(0), 36]; + yield 'ShortCircuit' => [new vs\ShortCircuit(new vs\IntVal(), new vs\GreaterThan(0)), 5]; yield 'FalseVal' => [new vs\FalseVal(), false]; yield 'Falsy' => [new vs\Falsy(), 0]; yield 'File' => [new vs\File(), __FILE__]; diff --git a/tests/src/Validators/Stub.php b/tests/src/Validators/Stub.php index 70485f550..89b002227 100644 --- a/tests/src/Validators/Stub.php +++ b/tests/src/Validators/Stub.php @@ -30,7 +30,7 @@ final class Stub extends Simple public array $validations; /** @var array */ - public array $inputs; + public array $inputs = []; public function __construct(bool ...$validations) { diff --git a/tests/unit/Validators/AllOfTest.php b/tests/unit/Validators/AllOfTest.php index 36309cdd7..ae7a0c1b9 100644 --- a/tests/unit/Validators/AllOfTest.php +++ b/tests/unit/Validators/AllOfTest.php @@ -4,10 +4,13 @@ * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-FileContributor: Alexandre Gomes Gaigalas + * SPDX-FileContributor: Andre Ramaciotti * SPDX-FileContributor: Gabriel Caruso * SPDX-FileContributor: Henrique Moody * SPDX-FileContributor: Nick Lombard + * SPDX-FileContributor: Pascal Borreli * SPDX-FileContributor: Torben Brodt + * SPDX-FileContributor: Vicente Mendoza */ declare(strict_types=1); @@ -15,13 +18,15 @@ namespace Respect\Validation\Validators; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; use Respect\Validation\Test\Validators\Stub; #[Group('validator')] #[CoversClass(AllOf::class)] -final class AllOfTest extends RuleTestCase +final class AllOfTest extends TestCase { /** @return iterable */ public static function providerForValidInput(): iterable @@ -39,4 +44,48 @@ public static function providerForInvalidInput(): iterable yield 'pass, fail, pass' => [new AllOf(Stub::pass(1), Stub::fail(1), Stub::pass(1)), []]; yield 'fail, pass, pass' => [new AllOf(Stub::fail(1), Stub::pass(1), Stub::pass(1)), []]; } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateShortCircuitValidInput(AllOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateShortCircuitValidInput(AllOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateValidInput(AllOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluate($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateValidInput(AllOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluate($input)->hasPassed); + } + + #[Test] + public function shouldShortCircuitStopEvaluatingAfterFirstFailure(): void + { + $stub1 = new Stub(false); + $stub2 = Stub::daze(); + $stub3 = Stub::daze(); + $validator = new AllOf($stub1, $stub2, $stub3); + + $result = $validator->evaluateShortCircuit([]); + + self::assertFalse($result->hasPassed); + self::assertCount(1, $stub1->inputs); + self::assertCount(0, $stub2->inputs); + self::assertCount(0, $stub3->inputs); + } } diff --git a/tests/unit/Validators/AllTest.php b/tests/unit/Validators/AllTest.php index 89980dc59..37e2c6de2 100644 --- a/tests/unit/Validators/AllTest.php +++ b/tests/unit/Validators/AllTest.php @@ -10,6 +10,7 @@ namespace Respect\Validation\Validators; +use ArrayIterator; use ArrayObject; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -62,4 +63,75 @@ public function shouldValidateInvalidInput(Stub $stub, mixed $input): void $validator = new All($stub); self::assertInvalidInput($validator, $input); } + + #[Test] + public function shouldShortCircuitOnFirstFailure(): void + { + $stub = new Stub(true, false, true); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit([1, 2, 3]); + + self::assertFalse($result->hasPassed); + self::assertCount(2, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitPassWhenAllItemsPass(): void + { + $stub = Stub::pass(3); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit([1, 2, 3]); + + self::assertTrue($result->hasPassed); + self::assertCount(3, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitFailForNonIterableInput(): void + { + $stub = Stub::daze(); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit('not an array'); + + self::assertFalse($result->hasPassed); + } + + #[Test] + public function shouldShortCircuitReturnIndeterminateForEmptyArray(): void + { + $stub = Stub::daze(); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit([]); + + self::assertTrue($result->hasPassed); + self::assertTrue($result->isIndeterminate); + } + + #[Test] + public function shouldShortCircuitWorkWithIterator(): void + { + $stub = new Stub(true, false, true); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit(new ArrayIterator([1, 2, 3])); + + self::assertFalse($result->hasPassed); + self::assertCount(2, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitIncludePathOnFailure(): void + { + $stub = new Stub(true, false, true); + $validator = new All($stub); + + $result = $validator->evaluateShortCircuit(['a' => 1, 'b' => 2, 'c' => 3]); + + self::assertFalse($result->hasPassed); + self::assertSame('b', $result->path?->value); + } } diff --git a/tests/unit/Validators/AnyOfTest.php b/tests/unit/Validators/AnyOfTest.php index d5f81c532..a256d4c85 100644 --- a/tests/unit/Validators/AnyOfTest.php +++ b/tests/unit/Validators/AnyOfTest.php @@ -18,13 +18,15 @@ namespace Respect\Validation\Validators; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; use Respect\Validation\Test\Validators\Stub; #[Group('validator')] #[CoversClass(AnyOf::class)] -final class AnyOfTest extends RuleTestCase +final class AnyOfTest extends TestCase { /** @return iterable */ public static function providerForValidInput(): iterable @@ -42,4 +44,48 @@ public static function providerForInvalidInput(): iterable yield 'fail, fail' => [new AnyOf(Stub::fail(1), Stub::fail(1)), []]; yield 'fail, fail, fail' => [new AnyOf(Stub::fail(1), Stub::fail(1), Stub::fail(1)), []]; } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateShortCircuitValidInput(AnyOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateShortCircuitValidInput(AnyOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateValidInput(AnyOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluate($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateValidInput(AnyOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluate($input)->hasPassed); + } + + #[Test] + public function shouldShortCircuitStopEvaluatingAfterFirstSuccess(): void + { + $stub1 = new Stub(true); + $stub2 = Stub::daze(); + $stub3 = Stub::daze(); + $validator = new AnyOf($stub1, $stub2, $stub3); + + $result = $validator->evaluateShortCircuit([]); + + self::assertTrue($result->hasPassed); + self::assertCount(1, $stub1->inputs); + self::assertCount(0, $stub2->inputs); + self::assertCount(0, $stub3->inputs); + } } diff --git a/tests/unit/Validators/EachTest.php b/tests/unit/Validators/EachTest.php index 4bc293654..87d053bed 100644 --- a/tests/unit/Validators/EachTest.php +++ b/tests/unit/Validators/EachTest.php @@ -15,9 +15,12 @@ namespace Respect\Validation\Validators; +use ArrayIterator; use ArrayObject; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; use Respect\Validation\Test\RuleTestCase; use Respect\Validation\Test\Validators\Stub; use stdClass; @@ -52,4 +55,86 @@ public static function providerForInvalidInput(): iterable [new Each(Stub::fail(5)), (object) ['foo' => true]], ]; } + + #[Test] + public function shouldShortCircuitOnFirstFailure(): void + { + $stub = new Stub(true, false, true, true, true); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit([1, 2, 3, 4, 5]); + + TestCase::assertFalse($result->hasPassed); + TestCase::assertCount(2, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitPassWhenAllItemsPass(): void + { + $stub = Stub::pass(5); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit([1, 2, 3, 4, 5]); + + TestCase::assertTrue($result->hasPassed); + TestCase::assertCount(5, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitFailForNonIterableInput(): void + { + $stub = Stub::daze(); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit('not an array'); + + TestCase::assertFalse($result->hasPassed); + } + + #[Test] + public function shouldShortCircuitReturnPassedForEmptyArray(): void + { + $stub = Stub::daze(); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit([]); + + TestCase::assertTrue($result->hasPassed); + } + + #[Test] + public function shouldShortCircuitWorkWithIterator(): void + { + $stub = new Stub(true, false, true, true, true); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit(new ArrayIterator([1, 2, 3, 4, 5])); + + TestCase::assertFalse($result->hasPassed); + TestCase::assertCount(2, $stub->inputs); + } + + #[Test] + public function shouldShortCircuitIncludePathOnFailure(): void + { + $stub = new Stub(true, false, true, true, true); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit([1, 2, 3, 4, 5]); + + TestCase::assertFalse($result->hasPassed); + TestCase::assertSame(1, $result->path?->value); + } + + #[Test] + public function shouldShortCircuitWorkWithNamedKeys(): void + { + $stub = new Stub(true, false, true); + $validator = new Each($stub); + + $result = $validator->evaluateShortCircuit(['a' => 1, 'b' => 2, 'c' => 3]); + + TestCase::assertFalse($result->hasPassed); + TestCase::assertSame('b', $result->path?->value); + } } diff --git a/tests/unit/Validators/KeySetTest.php b/tests/unit/Validators/KeySetTest.php index 26986cf20..9f855af2c 100644 --- a/tests/unit/Validators/KeySetTest.php +++ b/tests/unit/Validators/KeySetTest.php @@ -66,4 +66,51 @@ public static function providerForInvalidInput(): iterable yield 'correct keys, with failing rule' => [new KeySet(new Key('foo', Stub::fail(1))), ['foo' => 'bar']]; } + + #[Test] + public function itShouldShortCircuitOnFirstFailingKeyValidator(): void + { + $validator = new KeySet( + new Key('foo', Stub::fail(1)), + new Key('bar', Stub::daze()), + ); + + $result = $validator->evaluateShortCircuit(['foo' => 'value', 'bar' => 'value']); + + self::assertFalse($result->hasPassed); + } + + #[Test] + public function itShouldShortCircuitOnFirstExtraKey(): void + { + $validator = new KeySet(new KeyExists('foo')); + + $result = $validator->evaluateShortCircuit(['foo' => 'value', 'bar' => 'extra', 'baz' => 'extra']); + + self::assertFalse($result->hasPassed); + self::assertInstanceOf(KeyExists::class, $result->validator); + } + + #[Test] + public function itShouldShortCircuitOnNonArrayInput(): void + { + $validator = new KeySet(new Key('foo', Stub::daze())); + + $result = $validator->evaluateShortCircuit('not-an-array'); + + self::assertFalse($result->hasPassed); + } + + #[Test] + public function itShouldPassShortCircuitWhenAllKeysAreValid(): void + { + $validator = new KeySet( + new Key('foo', Stub::pass(1)), + new Key('bar', Stub::pass(1)), + ); + + $result = $validator->evaluateShortCircuit(['foo' => 'value', 'bar' => 'value']); + + self::assertTrue($result->hasPassed); + } } diff --git a/tests/unit/Validators/NoneOfTest.php b/tests/unit/Validators/NoneOfTest.php index 37d1778c8..dd72eb545 100644 --- a/tests/unit/Validators/NoneOfTest.php +++ b/tests/unit/Validators/NoneOfTest.php @@ -4,10 +4,13 @@ * SPDX-License-Identifier: MIT * SPDX-FileCopyrightText: (c) Respect Project Contributors * SPDX-FileContributor: Alexandre Gomes Gaigalas + * SPDX-FileContributor: Andre Ramaciotti * SPDX-FileContributor: Gabriel Caruso * SPDX-FileContributor: Henrique Moody * SPDX-FileContributor: Nick Lombard + * SPDX-FileContributor: Pascal Borreli * SPDX-FileContributor: Torben Brodt + * SPDX-FileContributor: Vicente Mendoza */ declare(strict_types=1); @@ -15,13 +18,15 @@ namespace Respect\Validation\Validators; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; use Respect\Validation\Test\Validators\Stub; #[Group('validator')] #[CoversClass(NoneOf::class)] -final class NoneOfTest extends RuleTestCase +final class NoneOfTest extends TestCase { /** @return iterable */ public static function providerForValidInput(): iterable @@ -39,4 +44,48 @@ public static function providerForInvalidInput(): iterable yield 'pass, fail, pass' => [new NoneOf(Stub::pass(1), Stub::fail(1), Stub::pass(1)), []]; yield 'fail, pass, pass' => [new NoneOf(Stub::fail(1), Stub::pass(1), Stub::pass(1)), []]; } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateShortCircuitValidInput(NoneOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateShortCircuitValidInput(NoneOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateValidInput(NoneOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluate($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateValidInput(NoneOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluate($input)->hasPassed); + } + + #[Test] + public function shouldShortCircuitStopEvaluatingAfterFirstFailure(): void + { + $stub1 = new Stub(true); + $stub2 = Stub::daze(); + $stub3 = Stub::daze(); + $validator = new NoneOf($stub1, $stub2, $stub3); + + $result = $validator->evaluateShortCircuit([]); + + self::assertFalse($result->hasPassed); + self::assertCount(1, $stub1->inputs); + self::assertCount(0, $stub2->inputs); + self::assertCount(0, $stub3->inputs); + } } diff --git a/tests/unit/Validators/OneOfTest.php b/tests/unit/Validators/OneOfTest.php index a00e90fdd..2286c4b11 100644 --- a/tests/unit/Validators/OneOfTest.php +++ b/tests/unit/Validators/OneOfTest.php @@ -19,13 +19,15 @@ namespace Respect\Validation\Validators; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; use Respect\Validation\Test\Validators\Stub; #[Group('validator')] #[CoversClass(OneOf::class)] -final class OneOfTest extends RuleTestCase +final class OneOfTest extends TestCase { /** @return iterable */ public static function providerForValidInput(): iterable @@ -44,4 +46,48 @@ public static function providerForInvalidInput(): iterable yield 'fail, fail, fail' => [new OneOf(Stub::fail(1), Stub::fail(1), Stub::fail(1)), []]; yield 'fail, pass, pass' => [new OneOf(Stub::fail(1), Stub::pass(1), Stub::pass(1)), []]; } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateShortCircuitValidInput(OneOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateShortCircuitValidInput(OneOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluateShortCircuit($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForValidInput')] + public function shouldEvaluateValidInput(OneOf $validator, mixed $input): void + { + self::assertTrue($validator->evaluate($input)->hasPassed); + } + + #[Test] + #[DataProvider('providerForInvalidInput')] + public function shouldNotEvaluateValidInput(OneOf $validator, mixed $input): void + { + self::assertFalse($validator->evaluate($input)->hasPassed); + } + + #[Test] + public function shouldShortCircuitStopEvaluatingAfterSecondSuccess(): void + { + $stub1 = new Stub(true); + $stub2 = new Stub(true); + $stub3 = Stub::daze(); + $validator = new OneOf($stub1, $stub2, $stub3); + + $result = $validator->evaluateShortCircuit([]); + + self::assertFalse($result->hasPassed); + self::assertCount(1, $stub1->inputs); + self::assertCount(1, $stub2->inputs); + self::assertCount(0, $stub3->inputs); + } } diff --git a/tests/unit/Validators/CircuitTest.php b/tests/unit/Validators/ShortCircuitTest.php similarity index 84% rename from tests/unit/Validators/CircuitTest.php rename to tests/unit/Validators/ShortCircuitTest.php index 3f253b7f4..bb78f8850 100644 --- a/tests/unit/Validators/CircuitTest.php +++ b/tests/unit/Validators/ShortCircuitTest.php @@ -20,21 +20,21 @@ use function rand; #[Group('validator')] -#[CoversClass(Circuit::class)] -final class CircuitTest extends TestCase +#[CoversClass(ShortCircuit::class)] +final class ShortCircuitTest extends TestCase { #[Test] #[DataProvider('providerForAnyValues')] public function itShouldValidateInputWhenAllValidatorsValidatesTheInput(mixed $input): void { - self::assertValidInput(new Circuit(Stub::pass(1), Stub::pass(1)), $input); + self::assertValidInput(new ShortCircuit(Stub::pass(1), Stub::pass(1)), $input); } #[Test] #[DataProvider('providerForFailingValidators')] public function itShouldExecuteValidatorsInSequenceUntilOneFails(Stub ...$stub): void { - $validator = new Circuit(...$stub); + $validator = new ShortCircuit(...$stub); self::assertInvalidInput($validator, rand()); } @@ -44,7 +44,7 @@ public function itShouldReturnTheResultOfTheFailingRule(): void { $input = rand(); - $validator = new Circuit(Stub::fail(1), Stub::daze()); + $validator = new ShortCircuit(Stub::fail(1), Stub::daze()); $actual = $validator->evaluate($input); $expected = Stub::fail(1)->evaluate($input);