├── .github └── workflows │ ├── coding-standards.yml │ ├── composer-require-checker.yml │ ├── kahlan-tests.yml │ └── psalm.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── crc-config.json ├── phpcs.xml.dist ├── psalm.xml ├── spec ├── Basic │ ├── ErrorsSpec.php │ ├── HasKeySpec.php │ ├── HasNotKeySpec.php │ ├── InArraySpec.php │ ├── IsArraySpec.php │ ├── IsAsAssertedSpec.php │ ├── IsBoolSpec.php │ ├── IsCallableSpec.php │ ├── IsFloatSpec.php │ ├── IsGreaterThanSpec.php │ ├── IsInstanceOfSpec.php │ ├── IsIntegerSpec.php │ ├── IsIterableSpec.php │ ├── IsLessThanSpec.php │ ├── IsNotNullSpec.php │ ├── IsNullSpec.php │ ├── IsNumericSpec.php │ ├── IsObjectSpec.php │ ├── IsResourceSpec.php │ ├── IsStringSpec.php │ ├── NonEmptySpec.php │ ├── RegexSpec.php │ ├── UserSpec.php │ └── ValidSpec.php ├── Combinator │ ├── AllSpec.php │ ├── AnyElementSpec.php │ ├── AnySpec.php │ ├── ApplySpec.php │ ├── AssociativeSpec.php │ ├── BindSpec.php │ ├── EveryElementSpec.php │ ├── FocusSpec.php │ ├── MapErrorsSpec.php │ ├── MapSpec.php │ ├── SequenceSpec.php │ └── TranslateErrorsSpec.php ├── Example │ └── ApplicativeMonadicSpec.php ├── Result │ ├── ValidationResultSpec.php │ └── functionsSpec.php ├── Translator │ ├── Combinator │ │ └── CoalesceSpec.php │ ├── ConstantTranslatorSpec.php │ ├── IdentityTranslatorSpec.php │ └── KeyValueTranslatorSpec.php └── functionsSpec.php └── src ├── Basic ├── Compare.php ├── ComposingAssertion.php ├── Errors.php ├── HasKey.php ├── HasNotKey.php ├── InArray.php ├── IsArray.php ├── IsAsAsserted.php ├── IsBool.php ├── IsCallable.php ├── IsFloat.php ├── IsGreaterThan.php ├── IsInstanceOf.php ├── IsInteger.php ├── IsIterable.php ├── IsLessThan.php ├── IsNotNull.php ├── IsNull.php ├── IsNumeric.php ├── IsObject.php ├── IsResource.php ├── IsString.php ├── NonEmpty.php ├── Regex.php └── Valid.php ├── Combinator ├── All.php ├── Any.php ├── AnyElement.php ├── Apply.php ├── Associative.php ├── Bind.php ├── EveryElement.php ├── Focus.php ├── Map.php ├── MapErrors.php ├── Sequence.php └── TranslateErrors.php ├── Equality.php ├── Result ├── ValidationResult.php └── functions.php ├── Translator ├── Combinator │ └── Coalesce.php ├── ConstantTranslator.php ├── IdentityTranslator.php ├── KeyValueTranslator.php └── Translator.php ├── Validation.php └── functions.php /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: "Check Coding Standards" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | coding-standards: 9 | name: "Check Coding Standards" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "7.4" 19 | - "8.0" 20 | operating-system: 21 | - "ubuntu-latest" 22 | 23 | steps: 24 | - name: "Checkout" 25 | uses: "actions/checkout@v2" 26 | 27 | - name: "Install PHP" 28 | uses: "shivammathur/setup-php@v2" 29 | with: 30 | coverage: "pcov" 31 | php-version: "${{ matrix.php-version }}" 32 | ini-values: memory_limit=-1 33 | tools: composer:v2, cs2pr 34 | 35 | - name: "Cache dependencies" 36 | uses: "actions/cache@v2" 37 | with: 38 | path: | 39 | ~/.composer/cache 40 | vendor 41 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 43 | 44 | - name: "Install lowest dependencies" 45 | if: ${{ matrix.dependencies == 'lowest' }} 46 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 47 | 48 | - name: "Install highest dependencies" 49 | if: ${{ matrix.dependencies == 'highest' }} 50 | run: "composer update --no-interaction --no-progress --no-suggest" 51 | 52 | - name: "Install locked dependencies" 53 | if: ${{ matrix.dependencies == 'locked' }} 54 | run: "composer install --no-interaction --no-progress --no-suggest" 55 | 56 | - name: "Coding Standard" 57 | run: "vendor/bin/phpcs -q --report=checkstyle | cs2pr" 58 | -------------------------------------------------------------------------------- /.github/workflows/composer-require-checker.yml: -------------------------------------------------------------------------------- 1 | name: "Check soft dependencies" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | coding-standards: 9 | name: "Check Coding Standards" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "7.4" 19 | - "8.0" 20 | operating-system: 21 | - "ubuntu-latest" 22 | 23 | steps: 24 | - name: "Checkout" 25 | uses: "actions/checkout@v2" 26 | 27 | - name: "Install PHP" 28 | uses: "shivammathur/setup-php@v2" 29 | with: 30 | coverage: "pcov" 31 | php-version: "${{ matrix.php-version }}" 32 | ini-values: memory_limit=-1 33 | tools: composer:v2, cs2pr 34 | 35 | - name: "Cache dependencies" 36 | uses: "actions/cache@v2" 37 | with: 38 | path: | 39 | ~/.composer/cache 40 | vendor 41 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 43 | 44 | - name: "Install lowest dependencies" 45 | if: ${{ matrix.dependencies == 'lowest' }} 46 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 47 | 48 | - name: "Install highest dependencies" 49 | if: ${{ matrix.dependencies == 'highest' }} 50 | run: "composer update --no-interaction --no-progress --no-suggest" 51 | 52 | - name: "Install locked dependencies" 53 | if: ${{ matrix.dependencies == 'locked' }} 54 | run: "composer install --no-interaction --no-progress --no-suggest" 55 | 56 | - name: "Coding Standard" 57 | run: "vendor/bin/composer-require-checker check --config-file crc-config.json" 58 | -------------------------------------------------------------------------------- /.github/workflows/kahlan-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Kahlan tests" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | kahlan: 9 | name: "Kahlan tests" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "lowest" 17 | - "highest" 18 | - "locked" 19 | php-version: 20 | - "7.4" 21 | - "8.0" 22 | operating-system: 23 | - "ubuntu-latest" 24 | 25 | steps: 26 | - name: "Checkout" 27 | uses: "actions/checkout@v2" 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: "Install PHP" 32 | uses: "shivammathur/setup-php@v2" 33 | with: 34 | coverage: "pcov" 35 | php-version: "${{ matrix.php-version }}" 36 | ini-values: memory_limit=-1 37 | tools: composer:v2, cs2pr 38 | 39 | - name: "Cache dependencies" 40 | uses: "actions/cache@v2" 41 | with: 42 | path: | 43 | ~/.composer/cache 44 | vendor 45 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 46 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 47 | 48 | - name: "Install lowest dependencies" 49 | if: ${{ matrix.dependencies == 'lowest' }} 50 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 51 | 52 | - name: "Install highest dependencies" 53 | if: ${{ matrix.dependencies == 'highest' }} 54 | run: "composer update --no-interaction --no-progress --no-suggest" 55 | 56 | - name: "Install locked dependencies" 57 | if: ${{ matrix.dependencies == 'locked' }} 58 | run: "composer install --no-interaction --no-progress --no-suggest" 59 | 60 | - name: "Tests" 61 | run: "vendor/bin/kahlan" -------------------------------------------------------------------------------- /.github/workflows/psalm.yml: -------------------------------------------------------------------------------- 1 | name: "Static Analysis by Psalm" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | static-analysis-psalm: 9 | name: "Static Analysis by Psalm" 10 | 11 | runs-on: ${{ matrix.operating-system }} 12 | 13 | strategy: 14 | matrix: 15 | dependencies: 16 | - "locked" 17 | php-version: 18 | - "7.4" 19 | - "8.0" 20 | operating-system: 21 | - "ubuntu-latest" 22 | 23 | steps: 24 | - name: "Checkout" 25 | uses: "actions/checkout@v2" 26 | 27 | - name: "Install PHP" 28 | uses: "shivammathur/setup-php@v2" 29 | with: 30 | coverage: "pcov" 31 | php-version: "${{ matrix.php-version }}" 32 | ini-values: memory_limit=-1 33 | tools: composer:v2, cs2pr 34 | 35 | - name: "Cache dependencies" 36 | uses: "actions/cache@v2" 37 | with: 38 | path: | 39 | ~/.composer/cache 40 | vendor 41 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 43 | 44 | - name: "Install lowest dependencies" 45 | if: ${{ matrix.dependencies == 'lowest' }} 46 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 47 | 48 | - name: "Install highest dependencies" 49 | if: ${{ matrix.dependencies == 'highest' }} 50 | run: "composer update --no-interaction --no-progress --no-suggest" 51 | 52 | - name: "Install locked dependencies" 53 | if: ${{ matrix.dependencies == 'locked' }} 54 | run: "composer install --no-interaction --no-progress --no-suggest" 55 | 56 | - name: "psalm" 57 | run: "vendor/bin/psalm --output-format=github --shepherd --stats" 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.1] 9 | 10 | - added `Associative` combinator 11 | 12 | ## [0.3.0] 13 | 14 | - added `Valid` validator 15 | - added `Errors` validator 16 | - added `IsGreaterThan` and `IsLessThan` validators 17 | - added `Apply` combinator 18 | - added `Bind` combinator 19 | - added `composer-require-check`, `phpcs`, `phpstan` and `psalm` to `CI` 20 | - use `MESSAGE` constant in all validators 21 | - use `phpstan` strict rules 22 | - use `psalm` 23 | - add `Equality` interface to check object equality 24 | - add `curry` and `uncurry` functions 25 | - add `lift`, `sdo` and `mdo` functions 26 | 27 | ## [0.2.3] - 2018-12-05 28 | 29 | - return received `$data` in `Focus` combinator 30 | 31 | ## [0.2.2] - 2018-12-03 32 | 33 | - return `$data` in valid result for `Any` combinator 34 | 35 | ## [0.2.1] - 2018-11-30 36 | 37 | - pass on `$context` in `Map` and `MapErrors` combinators 38 | 39 | ## [0.2.0] - 2018-11-22 40 | 41 | - set `All` constructor as private 42 | - set `Any` constructor as private 43 | - added `MapError` validator 44 | - added `HasNotKey` validator 45 | - added `InArray` validator 46 | - added `IsNumeric` validator 47 | - added `TranslateErrors` combinator 48 | 49 | ## [0.1.0] 50 | 51 | Basic validators and combinators -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marco Perone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marcosh/php-validation-dsl", 3 | "description": "A DSL for validating data in a functional fashion", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marco Perone", 9 | "email": "pasafama@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.4|^8.0", 14 | "webmozart/assert": "^1.10" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Marcosh\\PhpValidationDSL\\": "src/" 19 | }, 20 | "files": [ 21 | "src/functions.php", 22 | "src/Result/functions.php" 23 | ] 24 | }, 25 | "require-dev": { 26 | "ext-json": "*", 27 | "kahlan/kahlan": "^5.1", 28 | "squizlabs/php_codesniffer": "^3.6", 29 | "vimeo/psalm": "^4.10|dev-master", 30 | "maglnet/composer-require-checker": "^3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crc-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "array", 4 | "bool", 5 | "callable", 6 | "false", 7 | "null", 8 | "self", 9 | "static", 10 | "string", 11 | "true", 12 | "uncurry" 13 | ], 14 | "php-core-extensions" : [ 15 | "Core", 16 | "pcre", 17 | "Reflection", 18 | "standard" 19 | ], 20 | "scan-files" : [] 21 | } -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | src 15 | spec 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /spec/Basic/ErrorsSpec.php: -------------------------------------------------------------------------------- 1 | validate('anything')->equals(ValidationResult::errors([Errors::MESSAGE])))->toBeTruthy(); 16 | }); 17 | 18 | it('returns a custom error result if a custom formatter is passed', function () { 19 | $isString = Errors::withFormatter(function ($data) { 20 | return [(string) $data]; 21 | }); 22 | 23 | expect($isString->validate(true)->equals(ValidationResult::errors(['1'])))->toBeTruthy(); 24 | }); 25 | 26 | it('returns a translated error result if a translator is passed', function () { 27 | $isString = Errors::withTranslator(KeyValueTranslator::withDictionary([ 28 | Errors::MESSAGE => 'NOT VALID!' 29 | ])); 30 | 31 | expect($isString->validate(true)->equals(ValidationResult::errors(['NOT VALID!'])))->toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/Basic/HasKeySpec.php: -------------------------------------------------------------------------------- 1 | null]; 18 | 19 | expect($hasKey->validate($data)->equals(ValidationResult::valid($data)))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the key is not present', function () use ($hasKey) { 23 | $data = []; 24 | 25 | expect($hasKey->validate($data)->equals(ValidationResult::errors([HasKey::MISSING_KEY])))->toBeTruthy(); 26 | }); 27 | 28 | it('returns a custom error result if the key is not present and a custom formatter is passed', function () { 29 | $hasKey = HasKey::withKeyAndFormatter('key', function ($key, $data) { 30 | return [$key . json_encode($data)]; 31 | }); 32 | 33 | $data = []; 34 | 35 | expect($hasKey->validate($data)->equals(ValidationResult::errors(['key' . json_encode([])])))->toBeTruthy(); 36 | }); 37 | 38 | it('returns a translated error message if the key is not present and a translator is passed', function () { 39 | $hasKey = HasKey::withKeyAndTranslator( 40 | 'key', 41 | KeyValueTranslator::withDictionary([ 42 | HasKey::MISSING_KEY => 'MISSING KEY!' 43 | ]) 44 | ); 45 | 46 | $data = []; 47 | 48 | expect($hasKey->validate($data)->equals(ValidationResult::errors(['MISSING KEY!'])))->toBeTruthy(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /spec/Basic/HasNotKeySpec.php: -------------------------------------------------------------------------------- 1 | validate($data)->equals(ValidationResult::valid($data)))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the key is present', function () use ($hasNotKey) { 23 | $data = ['key' => null]; 24 | 25 | expect($hasNotKey->validate($data)->equals(ValidationResult::errors([HasNotKey::PRESENT_KEY])))->toBeTruthy(); 26 | }); 27 | 28 | it('returns a custom error result if the key is present and a custom formatter is passed', function () { 29 | $hasKey = HasNotKey::withKeyAndFormatter('key', function ($key, $data) { 30 | return [$key . json_encode($data)]; 31 | }); 32 | 33 | $data = ['key' => null]; 34 | 35 | expect($hasKey->validate($data)->equals(ValidationResult::errors(['key' . json_encode($data)])))->toBeTruthy(); 36 | }); 37 | 38 | it('returns a translated error message if the key is present and a translator is passed', function () { 39 | $hasKey = HasNotKey::withKeyAndTranslator( 40 | 'key', 41 | KeyValueTranslator::withDictionary([ 42 | HasNotKey::PRESENT_KEY => 'PRESENT KEY!' 43 | ]) 44 | ); 45 | 46 | $data = ['key' => null]; 47 | 48 | expect($hasKey->validate($data)->equals(ValidationResult::errors(['PRESENT KEY!'])))->toBeTruthy(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /spec/Basic/InArraySpec.php: -------------------------------------------------------------------------------- 1 | validate($data)->equals(ValidationResult::valid($data)))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the value is not present', function () use ($inArray) { 23 | $data = 'toni'; 24 | 25 | expect($inArray->validate($data)->equals(ValidationResult::errors([InArray::NOT_IN_ARRAY])))->toBeTruthy(); 26 | }); 27 | 28 | it('returns a custom error result if the value is not present and a custom formatter is passed', function () { 29 | $hasKey = InArray::withValuesAndFormatter(['gigi', 'bepi'], function ($values, $data) { 30 | return [json_encode($values) . $data]; 31 | }); 32 | 33 | $data = 'toni'; 34 | 35 | expect($hasKey->validate($data)->equals(ValidationResult::errors([json_encode(['gigi', 'bepi']) . 'toni']))) 36 | ->toBeTruthy(); 37 | }); 38 | 39 | it('returns a translated error message if the value is not present and a translator is passed', function () { 40 | $hasKey = InArray::withValuesAndTranslator( 41 | ['gigi', 'bepi'], 42 | KeyValueTranslator::withDictionary([ 43 | InArray::NOT_IN_ARRAY => 'NOT IN ARRAY!' 44 | ]) 45 | ); 46 | 47 | $data = 'toni'; 48 | 49 | expect($hasKey->validate($data)->equals(ValidationResult::errors(['NOT IN ARRAY!'])))->toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /spec/Basic/IsArraySpec.php: -------------------------------------------------------------------------------- 1 | validate([])->equals(ValidationResult::valid([])))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not an array', function () use ($isArray) { 19 | expect($isArray->validate(42)->equals(ValidationResult::errors([IsArray::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is not an array and a custom formatter is passed', function () { 23 | $isArray = IsArray::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isArray->validate(42)->equals(ValidationResult::errors(['42'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error message if the argument is not an array and a translator is passed', function () { 31 | $isArray = IsArray::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsArray::MESSAGE => 'NO ARRAY HERE!' 33 | ])); 34 | 35 | expect($isArray->validate(42)->equals(ValidationResult::errors(['NO ARRAY HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsAsAssertedSpec.php: -------------------------------------------------------------------------------- 1 | validate(42)->equals(ValidationResult::valid(42)))->toBeTruthy(); 18 | }); 19 | 20 | it('returns an error result if the assertion is not satisfied', function () use ($isAsAsserted) { 21 | expect($isAsAsserted->validate('fortytwo')->equals(ValidationResult::errors([IsAsAsserted::NOT_AS_ASSERTED]))) 22 | ->toBeTruthy(); 23 | }); 24 | 25 | it( 26 | 'returns a custom error result if the assertion is not satisfied and a custom formatter is passed', 27 | function () { 28 | $isAsAsserted = IsAsAsserted::withAssertionAndErrorFormatter( 29 | function ($data) { 30 | return $data === 42; 31 | }, 32 | function ($data) { 33 | return ['not 42']; 34 | } 35 | ); 36 | 37 | expect($isAsAsserted->validate('fortytwo')->equals(ValidationResult::errors(['not 42'])))->toBeTruthy(); 38 | } 39 | ); 40 | 41 | it( 42 | 'returns a translated error result if the assertion is not satisfied and a translator is passed', 43 | function () { 44 | $isAsAsserted = IsAsAsserted::withAssertionAndTranslator( 45 | function ($data) { 46 | return $data === 42; 47 | }, 48 | KeyValueTranslator::withDictionary([ 49 | IsAsAsserted::NOT_AS_ASSERTED => 'NOT AS ASSERTED!' 50 | ]) 51 | ); 52 | 53 | expect($isAsAsserted->validate('fortytwo')->equals(ValidationResult::errors(['NOT AS ASSERTED!']))) 54 | ->toBeTruthy(); 55 | } 56 | ); 57 | }); 58 | -------------------------------------------------------------------------------- /spec/Basic/IsBoolSpec.php: -------------------------------------------------------------------------------- 1 | validate(true)->equals(ValidationResult::valid(true)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not a boolean', function () use ($isBool) { 19 | expect($isBool->validate('true')->equals(ValidationResult::errors([IsBool::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the argument is not a boolean and a custom formatter is passed', function () { 23 | $isBool = IsBool::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isBool->validate('true')->equals(ValidationResult::errors(['true'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error message if the argument is not a boolean and a translator is passed', function () { 31 | $isArray = IsBool::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsBool::MESSAGE => 'NO BOOL HERE!' 33 | ])); 34 | 35 | expect($isArray->validate(42)->equals(ValidationResult::errors(['NO BOOL HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsCallableSpec.php: -------------------------------------------------------------------------------- 1 | validate('sprintf')->equals(ValidationResult::valid('sprintf')))->toBeTruthy(); 27 | }); 28 | 29 | it('returns a valid result if the argument is an anonymous function', function () use ($isCallable) { 30 | $function = function () { 31 | }; 32 | 33 | expect($isCallable->validate($function)->equals(ValidationResult::valid($function)))->toBeTruthy(); 34 | }); 35 | 36 | it('returns a valid result if the argument is a static method', function () use ($isCallable) { 37 | $staticMethod = [CallableFoo::class, 'bar']; 38 | 39 | expect($isCallable->validate($staticMethod)->equals(ValidationResult::valid($staticMethod)))->toBeTruthy(); 40 | }); 41 | 42 | it('returns a valid result if the argument is an instance method', function () use ($isCallable) { 43 | $instanceMethod = [new CallableFoo(), 'baz']; 44 | 45 | expect($isCallable->validate($instanceMethod)->equals(ValidationResult::valid($instanceMethod)))->toBeTruthy(); 46 | }); 47 | 48 | it('returns an error result if the argument is not a callable', function () use ($isCallable) { 49 | expect($isCallable->validate('true')->equals(ValidationResult::errors([IsCallable::MESSAGE]))) 50 | ->toBeTruthy(); 51 | }); 52 | 53 | it('returns a custom error if the argument is not a callable and a custom formatter is passed', function () { 54 | $isCallable = IsCallable::withFormatter(function ($data) { 55 | return [(string) $data]; 56 | }); 57 | 58 | expect($isCallable->validate('true')->equals(ValidationResult::errors(['true'])))->toBeTruthy(); 59 | }); 60 | 61 | it('returns a translated error if the argument is not a callable and a translator is passed', function () { 62 | $isCallable = IsCallable::withTranslator(KeyValueTranslator::withDictionary([ 63 | IsCallable::MESSAGE => 'NOT A CALLABLE!' 64 | ])); 65 | 66 | expect($isCallable->validate('true')->equals(ValidationResult::errors(['NOT A CALLABLE!'])))->toBeTruthy(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /spec/Basic/IsFloatSpec.php: -------------------------------------------------------------------------------- 1 | validate(12.34)->equals(ValidationResult::valid(12.34)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not a float', function () use ($isFloat) { 19 | expect($isFloat->validate('gigi')->equals(ValidationResult::errors([IsFloat::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is not a float and a custom formatter is passed', function () { 23 | $isFloat = IsFloat::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isFloat->validate('gigi')->equals(ValidationResult::errors(['gigi'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error result if the argument is not a float and a translator is passed', function () { 31 | $isFloat = IsFloat::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsFloat::MESSAGE => 'NO FLOAT HERE!' 33 | ])); 34 | 35 | expect($isFloat->validate('gigi')->equals(ValidationResult::errors(['NO FLOAT HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsGreaterThanSpec.php: -------------------------------------------------------------------------------- 1 | validate(87)->equals(ValidationResult::valid(87)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the value is equal to be bound', function () use ($isGreaterThan) { 19 | expect($isGreaterThan->validate(42)->equals(ValidationResult::errors([IsGreaterThan::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the value is less than be bound', function () use ($isGreaterThan) { 23 | expect($isGreaterThan->validate(23)->equals(ValidationResult::errors([IsGreaterThan::MESSAGE])))->toBeTruthy(); 24 | }); 25 | 26 | it( 27 | 'returns a custom error result if the argument is less than the bound and a custom formatter is passed', 28 | function () { 29 | $isGreaterThan = IsGreaterThan::withBoundAndFormatter(42, function ($bound, $data) { 30 | return [$bound . $data]; 31 | }); 32 | 33 | expect($isGreaterThan->validate(23)->equals(ValidationResult::errors(['4223'])))->toBeTruthy(); 34 | } 35 | ); 36 | 37 | it( 38 | 'returns a translated error result if the argument is less than the bound and a translator is passed', 39 | function () { 40 | $isGreaterThan = IsGreaterThan::withBoundAndTranslator(42, KeyValueTranslator::withDictionary([ 41 | IsGreaterThan::MESSAGE => 'LESS THAN 42!' 42 | ])); 43 | 44 | expect($isGreaterThan->validate('23')->equals(ValidationResult::errors(['LESS THAN 42!'])))->toBeTruthy(); 45 | } 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/Basic/IsInstanceOfSpec.php: -------------------------------------------------------------------------------- 1 | validate($instance)->equals(ValidationResult::valid($instance)))->toBeTruthy(); 24 | }); 25 | 26 | it('returns an error result if the argument is not a string', function () use ($isInstanceOf) { 27 | expect( 28 | $isInstanceOf->validate(new \stdClass())->equals(ValidationResult::errors([IsInstanceOf::NOT_AN_INSTANCE])) 29 | )->toBeTruthy(); 30 | }); 31 | 32 | it('returns a custom error result if the argument is not a string and a custom formatter is passed', function () { 33 | $isInstanceOf = IsInstanceOf::withClassNameAndFormatter( 34 | InstanceFoo::class, 35 | function (string $className, $data) { 36 | return [$className . json_encode($data)]; 37 | } 38 | ); 39 | 40 | expect($isInstanceOf->validate(new \stdClass())->equals(ValidationResult::errors([InstanceFoo::class . '{}']))) 41 | ->toBeTruthy(); 42 | }); 43 | 44 | it('returns a translated error result if the argument is not a string and a translator is passed', function () { 45 | $isInstanceOf = IsInstanceOf::withClassNameAndTranslator( 46 | InstanceFoo::class, 47 | KeyValueTranslator::withDictionary([ 48 | IsInstanceOf::NOT_AN_INSTANCE => 'NO INSTANCE HERE!' 49 | ]) 50 | ); 51 | 52 | expect($isInstanceOf->validate(new \stdClass())->equals(ValidationResult::errors(['NO INSTANCE HERE!']))) 53 | ->toBeTruthy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/Basic/IsIntegerSpec.php: -------------------------------------------------------------------------------- 1 | validate(42)->equals(ValidationResult::valid(42)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not an integer', function () use ($isInteger) { 19 | expect($isInteger->validate('gigi')->equals(ValidationResult::errors([IsInteger::MESSAGE]))) 20 | ->toBeTruthy(); 21 | }); 22 | 23 | it('returns a custom error result if the argument is not an int and a custom formatter is passed', function () { 24 | $isInteger = IsInteger::withFormatter(function ($data) { 25 | return [(string) $data]; 26 | }); 27 | 28 | expect($isInteger->validate('gigi')->equals(ValidationResult::errors(['gigi'])))->toBeTruthy(); 29 | }); 30 | 31 | it('returns a translated error result if the argument is not an int and a translator is passed', function () { 32 | $isInteger = IsInteger::withTranslator(KeyValueTranslator::withDictionary([ 33 | IsInteger::MESSAGE => 'NO INTEGER HERE!' 34 | ])); 35 | 36 | expect($isInteger->validate('gigi')->equals(ValidationResult::errors(['NO INTEGER HERE!'])))->toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /spec/Basic/IsIterableSpec.php: -------------------------------------------------------------------------------- 1 | validate([])->equals(ValidationResult::valid([])))->toBeTruthy(); 16 | }); 17 | 18 | it('returns a valid result if the argument is a Traversable', function () use ($isIterable) { 19 | $iterator = new \ArrayIterator(); 20 | 21 | expect($isIterable->validate($iterator)->equals(ValidationResult::valid($iterator)))->toBeTruthy(); 22 | }); 23 | 24 | it('returns an error result if the argument is not an iterable', function () use ($isIterable) { 25 | expect($isIterable->validate('gigi')->equals(ValidationResult::errors([IsIterable::MESSAGE]))) 26 | ->toBeTruthy(); 27 | }); 28 | 29 | it('returns a custom error result if the argument is not iterable and a custom formatter is passed', function () { 30 | $isIterable = IsIterable::withFormatter(function ($data) { 31 | return [(string) $data]; 32 | }); 33 | 34 | expect($isIterable->validate('gigi')->equals(ValidationResult::errors(['gigi'])))->toBeTruthy(); 35 | }); 36 | 37 | it('returns a translated error result if the argument is not iterable and a translator is passed', function () { 38 | $isIterable = IsIterable::withTranslator(KeyValueTranslator::withDictionary([ 39 | IsIterable::MESSAGE => 'NO ITERABLE HERE!' 40 | ])); 41 | 42 | expect($isIterable->validate('gigi')->equals(ValidationResult::errors(['NO ITERABLE HERE!'])))->toBeTruthy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/Basic/IsLessThanSpec.php: -------------------------------------------------------------------------------- 1 | validate(23)->equals(ValidationResult::valid(23)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the value is equal to be bound', function () use ($isLessThan) { 19 | expect($isLessThan->validate(42)->equals(ValidationResult::errors([IsLessThan::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the value is greater than be bound', function () use ($isLessThan) { 23 | expect($isLessThan->validate(87)->equals(ValidationResult::errors([IsLessThan::MESSAGE])))->toBeTruthy(); 24 | }); 25 | 26 | it( 27 | 'returns a custom error result if the argument is greater than the bound and a custom formatter is passed', 28 | function () { 29 | $isGreaterThan = IsLessThan::withBoundAndFormatter(42, function ($bound, $data) { 30 | return [$bound . $data]; 31 | }); 32 | 33 | expect($isGreaterThan->validate(87)->equals(ValidationResult::errors(['4287'])))->toBeTruthy(); 34 | } 35 | ); 36 | 37 | it( 38 | 'returns a translated error result if the argument is greater than the bound and a translator is passed', 39 | function () { 40 | $isGreaterThan = IsLessThan::withBoundAndTranslator(42, KeyValueTranslator::withDictionary([ 41 | IsLessThan::MESSAGE => 'MORE THAN 42!' 42 | ])); 43 | 44 | expect($isGreaterThan->validate('87')->equals(ValidationResult::errors(['MORE THAN 42!'])))->toBeTruthy(); 45 | } 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/Basic/IsNotNullSpec.php: -------------------------------------------------------------------------------- 1 | validate(42)->equals(ValidationResult::valid(42)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not null', function () use ($isNotNull) { 19 | expect($isNotNull->validate(null)->equals(ValidationResult::errors([IsNotNull::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is not null and a custom formatter is passed', function () { 23 | $isNotNull = IsNotNull::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isNotNull->validate(null)->equals(ValidationResult::errors([''])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error result if the argument is not null and a translator is passed', function () { 31 | $isNotNull = IsNotNull::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsNotNull::MESSAGE => 'NULL HERE!' 33 | ])); 34 | 35 | expect($isNotNull->validate(null)->equals(ValidationResult::errors(['NULL HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsNullSpec.php: -------------------------------------------------------------------------------- 1 | validate(null)->equals(ValidationResult::valid(null)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not null', function () use ($isNull) { 19 | expect($isNull->validate(42)->equals(ValidationResult::errors([IsNull::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is null and a custom formatter is passed', function () { 23 | $isNull = IsNull::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isNull->validate(42)->equals(ValidationResult::errors(['42'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error result if the argument is null and a translator is passed', function () { 31 | $isNull = IsNull::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsNull::MESSAGE => 'NO NULL HERE!' 33 | ])); 34 | 35 | expect($isNull->validate(42)->equals(ValidationResult::errors(['NO NULL HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsNumericSpec.php: -------------------------------------------------------------------------------- 1 | validate(12.34)->equals(ValidationResult::valid(12.34)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not numeric', function () use ($isNumeric) { 19 | expect($isNumeric->validate('gigi')->equals(ValidationResult::errors([IsNumeric::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is not numeric and a custom formatter is passed', function () { 23 | $isFloat = IsNumeric::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isFloat->validate('gigi')->equals(ValidationResult::errors(['gigi'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error result if the argument is not numeric and a translator is passed', function () { 31 | $isFloat = IsNumeric::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsNumeric::MESSAGE => 'NO NUMERIC HERE!' 33 | ])); 34 | 35 | expect($isFloat->validate('gigi')->equals(ValidationResult::errors(['NO NUMERIC HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/IsObjectSpec.php: -------------------------------------------------------------------------------- 1 | validate($object)->equals(ValidationResult::valid($object)))->toBeTruthy(); 18 | }); 19 | 20 | it('returns an error result if the argument is not an object', function () use ($isObject) { 21 | expect($isObject->validate(true)->equals(ValidationResult::errors([IsObject::MESSAGE])))->toBeTruthy(); 22 | }); 23 | 24 | it('returns a custom error result if the argument is not an object and a custom formatter is passed', function () { 25 | $isObject = IsObject::withFormatter(function ($data) { 26 | return [(string) $data]; 27 | }); 28 | 29 | expect($isObject->validate(true)->equals(ValidationResult::errors(['1'])))->toBeTruthy(); 30 | }); 31 | 32 | it('returns a translated error result if the argument is not an object and a translator is passed', function () { 33 | $isObject = IsObject::withTranslator(KeyValueTranslator::withDictionary([ 34 | IsObject::MESSAGE => 'NO OBJECT HERE!' 35 | ])); 36 | 37 | expect($isObject->validate(true)->equals(ValidationResult::errors(['NO OBJECT HERE!'])))->toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/Basic/IsResourceSpec.php: -------------------------------------------------------------------------------- 1 | validate($resource)->equals(ValidationResult::valid($resource)))->toBeTruthy(); 20 | }); 21 | 22 | it('returns an error result if the argument is not a resource', function () use ($isResource) { 23 | expect($isResource->validate('gigi')->equals(ValidationResult::errors([IsResource::MESSAGE]))) 24 | ->toBeTruthy(); 25 | }); 26 | 27 | it('returns a custom error result if the argument is not a resource and custom formatter is passed', function () { 28 | $isResource = IsResource::withFormatter(function ($data) { 29 | return [(string) $data]; 30 | }); 31 | 32 | expect($isResource->validate('gigi')->equals(ValidationResult::errors(['gigi'])))->toBeTruthy(); 33 | }); 34 | 35 | it('returns a translated error result if the argument is not a resource and translator is passed', function () { 36 | $isResource = IsResource::withTranslator(KeyValueTranslator::withDictionary([ 37 | IsResource::MESSAGE => 'NO RESOURCE HERE!' 38 | ])); 39 | 40 | expect($isResource->validate('gigi')->equals(ValidationResult::errors(['NO RESOURCE HERE!'])))->toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /spec/Basic/IsStringSpec.php: -------------------------------------------------------------------------------- 1 | validate('true')->equals(ValidationResult::valid('true')))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is not a string', function () use ($isString) { 19 | expect($isString->validate(true)->equals(ValidationResult::errors([IsString::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is not a string and a custom formatter is passed', function () { 23 | $isString = IsString::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isString->validate(true)->equals(ValidationResult::errors(['1'])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error result if the argument is not a string and a translator is passed', function () { 31 | $isString = IsString::withTranslator(KeyValueTranslator::withDictionary([ 32 | IsString::MESSAGE => 'NO STRING HERE!' 33 | ])); 34 | 35 | expect($isString->validate(true)->equals(ValidationResult::errors(['NO STRING HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/NonEmptySpec.php: -------------------------------------------------------------------------------- 1 | validate(42)->equals(ValidationResult::valid(42)))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the argument is empty', function () use ($nonEmpty) { 19 | expect($nonEmpty->validate([])->equals(ValidationResult::errors([NonEmpty::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the argument is empty and a custom formatter is passed', function () { 23 | $isArray = NonEmpty::withFormatter(function ($data) { 24 | return [(string) $data]; 25 | }); 26 | 27 | expect($isArray->validate(null)->equals(ValidationResult::errors([''])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a translated error message if the argument is empty and a translator is passed', function () { 31 | $isArray = NonEmpty::withTranslator(KeyValueTranslator::withDictionary([ 32 | NonEmpty::MESSAGE => 'EMPTY HERE!' 33 | ])); 34 | 35 | expect($isArray->validate([])->equals(ValidationResult::errors(['EMPTY HERE!'])))->toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/Basic/RegexSpec.php: -------------------------------------------------------------------------------- 1 | validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 16 | }); 17 | 18 | it('returns an error result if the pattern does not match', function () use ($regex) { 19 | expect($regex->validate('gigi@zucon')->equals(ValidationResult::errors([Regex::MESSAGE])))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a custom error result if the pattern does not match and a custom formatter is passed', function () { 23 | $regex = Regex::withPatternAndFormatter('/^[\p{L} ]*$/u', function ($pattern, $data) { 24 | return [$pattern . $data]; 25 | }); 26 | 27 | expect($regex->validate('gigi@zucon')->equals(ValidationResult::errors(['/^[\p{L} ]*$/u' . 'gigi@zucon']))) 28 | ->toBeTruthy(); 29 | }); 30 | 31 | it('returns a translated error result if the pattern does not match and a translator is passed', function () { 32 | $regex = Regex::withPatternAndTranslator( 33 | '/^[\p{L} ]*$/u', 34 | KeyValueTranslator::withDictionary([ 35 | Regex::MESSAGE => 'NO MATCH HERE!' 36 | ]) 37 | ); 38 | 39 | expect($regex->validate('gigi@zucon')->equals(ValidationResult::errors(['NO MATCH HERE!'])))->toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/Basic/UserSpec.php: -------------------------------------------------------------------------------- 1 | = 0; 44 | }) 45 | ]) 46 | ) 47 | ]) 48 | ]) 49 | ]); 50 | 51 | it('fails if the data is not an array', function () use ($userValidation) { 52 | expect($userValidation->validate('gigi')->equals(ValidationResult::errors([IsArray::MESSAGE]))) 53 | ->toBeTruthy(); 54 | }); 55 | 56 | it('fails if the name field is missing', function () use ($userValidation) { 57 | expect($userValidation->validate(['age' => 18])->equals(ValidationResult::errors([HasKey::MISSING_KEY]))) 58 | ->toBeTruthy(); 59 | }); 60 | 61 | it('fails if the name field is not a string', function () use ($userValidation) { 62 | expect( 63 | $userValidation->validate(['name' => 42, 'age' => 42]) 64 | ->equals(ValidationResult::errors([IsString::MESSAGE])) 65 | )->toBeTruthy(); 66 | }); 67 | 68 | it('fails if the name field is an empty string', function () use ($userValidation) { 69 | expect( 70 | $userValidation->validate(['name' => '', 'age' => 42]) 71 | ->equals(ValidationResult::errors([NonEmpty::MESSAGE])) 72 | )->toBeTruthy(); 73 | }); 74 | 75 | it('fails if the age field is missing', function () use ($userValidation) { 76 | expect($userValidation->validate(['name' => 'gigi'])->equals(ValidationResult::errors([HasKey::MISSING_KEY]))) 77 | ->toBeTruthy(); 78 | }); 79 | 80 | it('fails if the age field is not an integer', function () use ($userValidation) { 81 | expect( 82 | $userValidation->validate(['name' => 'gigi', 'age' => 'gigi']) 83 | ->equals(ValidationResult::errors([IsInteger::MESSAGE])) 84 | )->toBeTruthy(); 85 | }); 86 | 87 | it('fails if the age field is negative', function () use ($userValidation) { 88 | expect( 89 | $userValidation->validate(['name' => 'gigi', 'age' => -3]) 90 | ->equals(ValidationResult::errors([IsAsAsserted::NOT_AS_ASSERTED])) 91 | )->toBeTruthy(); 92 | }); 93 | 94 | it( 95 | 'succeeds if the name is a non empty string and the age is a positive integer', 96 | function () use ($userValidation) { 97 | expect( 98 | $userValidation->validate(['name' => 'gigi', 'age' => 42]) 99 | ->equals(ValidationResult::valid(['name' => 'gigi', 'age' => 42])) 100 | )->toBeTruthy(); 101 | } 102 | ); 103 | }); 104 | -------------------------------------------------------------------------------- /spec/Basic/ValidSpec.php: -------------------------------------------------------------------------------- 1 | validate('anything')->equals(ValidationResult::valid('anything')))->toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /spec/Combinator/AllSpec.php: -------------------------------------------------------------------------------- 1 | validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 20 | }); 21 | 22 | it('returns a valid result if every validator succeeds', function () { 23 | $all = All::validations([ 24 | new IsString(), 25 | Regex::withPattern('/^[\p{L} ]*$/u') 26 | ]); 27 | 28 | expect($all->validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 29 | }); 30 | 31 | it('returns an error result if one validator fails with all the errors combined', function () { 32 | $all = All::validations([ 33 | new IsString(), 34 | new IsBool() 35 | ]); 36 | 37 | expect($all->validate(42)->equals(ValidationResult::errors([ 38 | IsString::MESSAGE, 39 | IsBool::MESSAGE 40 | ])))->toBeTruthy(); 41 | }); 42 | 43 | it('returns a custom error result if one validator fails using the custom error formatter', function () { 44 | $all = All::validationsWithFormatter([ 45 | new IsString(), 46 | new IsBool() 47 | ], function (...$errors) { 48 | return [json_encode($errors)]; 49 | }); 50 | 51 | expect( 52 | $all->validate(42)->equals(ValidationResult::errors(['[["[[],[\"is-string.not-a-string\"]]"],["is-bool.not-a-bool"]]'])) 53 | )->toBeTruthy(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/Combinator/AnyElementSpec.php: -------------------------------------------------------------------------------- 1 | validate([])->equals(ValidationResult::errors([])))->toBeTruthy(); 16 | }); 17 | 18 | it('returns a valid result if the validation on one element succeeds', function () { 19 | $anyElement = AnyElement::validation(new IsString()); 20 | 21 | expect($anyElement->validate([42, 'bepi'])->equals(ValidationResult::valid([42, 'bepi'])))->toBeTruthy(); 22 | }); 23 | 24 | it('returns a valid result if the validation on the first element succeeds', function () { 25 | $anyElement = AnyElement::validation(new IsString()); 26 | 27 | expect($anyElement->validate(['gigi', 42])->equals(ValidationResult::valid(['gigi', 42])))->toBeTruthy(); 28 | }); 29 | 30 | it('returns a valid result if the validation succeeds on both elements', function () { 31 | $anyElement = AnyElement::validation(new IsString()); 32 | 33 | expect($anyElement->validate(['gigi', 'bepi'])->equals(ValidationResult::valid(['gigi', 'bepi'])))->toBeTruthy(); 34 | }); 35 | 36 | it('returns an error result if every element fails the validation', function () { 37 | $anyElement = AnyElement::validation(new IsString()); 38 | 39 | expect($anyElement->validate([true, 42])->equals(ValidationResult::errors([ 40 | 0 => [IsString::MESSAGE], 41 | 1 => [IsString::MESSAGE] 42 | ])))->toBeTruthy(); 43 | }); 44 | 45 | it( 46 | 'returns a custom error result if every element fails the validation and a custom formatter is passed', 47 | function () { 48 | $anyElement = AnyElement::validationWithFormatter( 49 | new IsString(), 50 | function ($key, $resultMessages, $validationMessages) { 51 | $resultMessages[] = $key . $validationMessages[0]; 52 | 53 | return $resultMessages; 54 | } 55 | ); 56 | 57 | expect($anyElement->validate([true, 42])->equals(ValidationResult::errors([ 58 | 0 . IsString::MESSAGE, 59 | 1 . IsString::MESSAGE 60 | ])))->toBeTruthy(); 61 | } 62 | ); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/Combinator/AnySpec.php: -------------------------------------------------------------------------------- 1 | validate('gigi')->equals(ValidationResult::errors([Any::NOT_EVEN_ONE => []])))->toBeTruthy(); 18 | }); 19 | 20 | it('returns a valid result if one validator succeeds', function () { 21 | $any = Any::validations([ 22 | new IsString(), 23 | new IsBool() 24 | ]); 25 | 26 | expect($any->validate(true)->equals(ValidationResult::valid(true)))->toBeTruthy(); 27 | }); 28 | 29 | it('returns an error result if every validator fails with all the errors combined', function () { 30 | $any = Any::validations([ 31 | new IsString(), 32 | new IsBool() 33 | ]); 34 | 35 | expect($any->validate(42)->equals(ValidationResult::errors([ 36 | Any::NOT_EVEN_ONE => [ 37 | IsString::MESSAGE, 38 | IsBool::MESSAGE 39 | ] 40 | ])))->toBeTruthy(); 41 | }); 42 | 43 | it( 44 | 'returns a custom error result if every validator fails with the errors combined by the error formatter', 45 | function () { 46 | $any = Any::validationsWithFormatter([ 47 | new IsString(), 48 | new IsBool() 49 | ], function (array $messages) { 50 | return $messages; 51 | }); 52 | 53 | expect($any->validate(42)->equals(ValidationResult::errors([ 54 | IsString::MESSAGE, 55 | IsBool::MESSAGE 56 | ])))->toBeTruthy(); 57 | } 58 | ); 59 | 60 | it( 61 | 'returns a translated error result if every validator fails with the errors combined by the translator', 62 | function () { 63 | $any = Any::validationsWithTranslator([ 64 | new IsString(), 65 | new IsBool() 66 | ], KeyValueTranslator::withDictionary([ 67 | Any::NOT_EVEN_ONE => 'NOT EVEN ONE!' 68 | ])); 69 | 70 | expect($any->validate(42)->equals(ValidationResult::errors([ 71 | 'NOT EVEN ONE!' => [ 72 | IsString::MESSAGE, 73 | IsBool::MESSAGE 74 | ] 75 | ])))->toBeTruthy(); 76 | } 77 | ); 78 | }); 79 | -------------------------------------------------------------------------------- /spec/Combinator/ApplySpec.php: -------------------------------------------------------------------------------- 1 | validate('abc')->equals(ValidationResult::valid(3)))->toBeTruthy(); 18 | }); 19 | 20 | it('does not modify the result of a failed validation', function () { 21 | $f = ValidationResult::valid(function (string $x) { 22 | return strlen($x); 23 | }); 24 | 25 | expect( 26 | Apply::to(new IsString(), $f)->validate(42)->equals(ValidationResult::errors([IsString::MESSAGE])) 27 | )->toBeTruthy(); 28 | }); 29 | 30 | it('does return a failed validation if the applied function is already failed', function () { 31 | $f = ValidationResult::errors(['gigi']); 32 | 33 | expect(Apply::to(new IsString(), $f)->validate('abc')->equals($f))->toBeTruthy(); 34 | }); 35 | 36 | it( 37 | 'does return a failed validation cumulating errors if both the validation and the applied function are failed', 38 | function () { 39 | $f = ValidationResult::errors(['gigi']); 40 | 41 | expect(Apply::to(new IsString(), $f)->validate(42)->equals(ValidationResult::errors([ 42 | 'gigi', 43 | IsString::MESSAGE 44 | ])))->toBeTruthy(); 45 | } 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/Combinator/AssociativeSpec.php: -------------------------------------------------------------------------------- 1 | Sequence::validations([ 20 | new IsString(), 21 | new NonEmpty() 22 | ]), 23 | 'age' => Sequence::validations([ 24 | new IsInteger(), 25 | IsGreaterThan::withBound(0) 26 | ]) 27 | ]); 28 | 29 | it('returns a valid result is the data pass validation', function () use ($validation) { 30 | $data = [ 31 | 'name' => 'gigi', 32 | 'age' => 42 33 | ]; 34 | 35 | expect($validation->validate($data)->equals(ValidationResult::valid($data)))->toBeTruthy(); 36 | }); 37 | 38 | it('returns an error result if the data is not an array', function () use ($validation) { 39 | expect($validation->validate('gigi')->equals(ValidationResult::errors([IsArray::MESSAGE])))->toBeTruthy(); 40 | }); 41 | 42 | it('returns an error result if a key is missing', function () use ($validation) { 43 | $errors = [ 44 | 'name' => [HasKey::MISSING_KEY], 45 | 'age' => [HasKey::MISSING_KEY] 46 | ]; 47 | 48 | expect($validation->validate([])->equals(ValidationResult::errors($errors)))->toBeTruthy(); 49 | }); 50 | 51 | it('returns an error result if a value fails its own validation', function () use ($validation) { 52 | $data = [ 53 | 'name' => 42, 54 | 'age' => 'gigi' 55 | ]; 56 | 57 | $errors = [ 58 | 'name' => [IsString::MESSAGE], 59 | 'age' => [IsInteger::MESSAGE] 60 | ]; 61 | 62 | expect($validation->validate($data)->equals(ValidationResult::errors($errors)))->toBeTruthy(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/Combinator/BindSpec.php: -------------------------------------------------------------------------------- 1 | validate('abc'); 15 | }; 16 | 17 | it('does modify the result of a correct validation', function () use ($f) { 18 | expect(Bind::to(new IsString(), $f)->validate('/[abc]+/')->equals(ValidationResult::valid('abc'))) 19 | ->toBeTruthy(); 20 | }); 21 | 22 | it('does not modify the result of a failed validation', function () use ($f) { 23 | expect( 24 | Bind::to(new IsString(), $f)->validate('/[def]+/')->equals(ValidationResult::errors([Regex::MESSAGE])) 25 | )->toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /spec/Combinator/EveryElementSpec.php: -------------------------------------------------------------------------------- 1 | validate([])->equals(ValidationResult::valid([])))->toBeTruthy(); 16 | }); 17 | 18 | it('returns a valid result if the validation on every element succeeds', function () { 19 | $everyElement = EveryElement::validation(new IsString()); 20 | 21 | expect($everyElement->validate(['gigi', 'bepi'])->equals(ValidationResult::valid(['gigi', 'bepi']))) 22 | ->toBeTruthy(); 23 | }); 24 | 25 | it('returns an error result if one element fails the validation', function () { 26 | $everyElement = EveryElement::validation(new IsString()); 27 | 28 | expect($everyElement->validate(['gigi', 42])->equals(ValidationResult::errors([ 29 | 1 => [IsString::MESSAGE] 30 | ])))->toBeTruthy(); 31 | }); 32 | 33 | it( 34 | 'returns a custom error result if one element fails the validation and a custom formatter is passed', 35 | function () { 36 | $everyElement = EveryElement::validationWithFormatter( 37 | new IsString(), 38 | function ($key, $resultMessages, $validationMessages) { 39 | $resultMessages[] = $key . $validationMessages[0]; 40 | 41 | return $resultMessages; 42 | } 43 | ); 44 | 45 | expect($everyElement->validate([true, 42])->equals(ValidationResult::errors([ 46 | 0 . IsString::MESSAGE, 47 | 1 . IsString::MESSAGE 48 | ])))->toBeTruthy(); 49 | } 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /spec/Combinator/FocusSpec.php: -------------------------------------------------------------------------------- 1 | validate(['nested' => 'gigi'])->equals(ValidationResult::valid(['nested' => 'gigi']))) 21 | ->toBeTruthy(); 22 | }); 23 | 24 | it('returns an error result if the mapped data is not valid', function () { 25 | $focus = Focus::on( 26 | function ($data) { 27 | return $data['nested']; 28 | }, 29 | new IsString() 30 | ); 31 | 32 | expect($focus->validate(['nested' => 42])->equals(ValidationResult::errors([IsString::MESSAGE]))) 33 | ->toBeTruthy(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/Combinator/MapErrorsSpec.php: -------------------------------------------------------------------------------- 1 | validate('gigi')->equals(ValidationResult::valid('gigi'))) 18 | ->toBeTruthy(); 19 | }); 20 | 21 | it('does modify the result of a failed validation', function () use ($map) { 22 | expect( 23 | MapErrors::to(new IsString(), $map)->validate(42)->equals(ValidationResult::errors([ 24 | strtoupper(IsString::MESSAGE) 25 | ])) 26 | )->toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /spec/Combinator/MapSpec.php: -------------------------------------------------------------------------------- 1 | validate(42)->equals((new IsString())->validate(42))) 14 | ->toBeTruthy(); 15 | }); 16 | 17 | it('does modify the result of a correct validation', function () { 18 | expect(Map::to(new IsString(), 'strtolower')->validate('GIGI')->equals(ValidationResult::valid('gigi'))) 19 | ->toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /spec/Combinator/SequenceSpec.php: -------------------------------------------------------------------------------- 1 | validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 18 | }); 19 | 20 | it('returns a valid result if every validator succeeds', function () { 21 | $sequence = Sequence::validations([ 22 | new IsString(), 23 | Regex::withPattern('/^[\p{L} ]*$/u') 24 | ]); 25 | 26 | expect($sequence->validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 27 | }); 28 | 29 | it('returns an error result if one validator fails with just the first error', function () { 30 | $sequence = Sequence::validations([ 31 | new IsString(), 32 | new IsBool() 33 | ]); 34 | 35 | expect($sequence->validate(42)->equals(ValidationResult::errors([ 36 | IsString::MESSAGE 37 | ])))->toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/Combinator/TranslateErrorsSpec.php: -------------------------------------------------------------------------------- 1 | 'Only strings here!', 19 | IsInteger::MESSAGE => 'Only integers here!' 20 | ]); 21 | 22 | $validator = TranslateErrors::validationWithTranslator(new IsString(), $translator); 23 | 24 | it('does not modify the result of a correct validation', function () use ($validator) { 25 | expect($validator->validate('gigi')->equals(ValidationResult::valid('gigi')))->toBeTruthy(); 26 | }); 27 | 28 | it('translates the result of a failed validation', function () use ($validator) { 29 | expect($validator->validate(42)->equals(ValidationResult::errors(['Only strings here!'])))->toBeTruthy(); 30 | }); 31 | 32 | it('translates nested results of a failed validation', function () use ($translator) { 33 | $validator = TranslateErrors::validationWithTranslator( 34 | All::validations([ 35 | Focus::on( 36 | function ($data) { 37 | return $data['a']; 38 | }, 39 | MapErrors::to( 40 | new IsString(), 41 | function (array $messages) { 42 | return ['a' => $messages]; 43 | } 44 | ) 45 | ), 46 | Focus::on( 47 | function ($data) { 48 | return $data['b']; 49 | }, 50 | MapErrors::to( 51 | new IsInteger(), 52 | function (array $messages) { 53 | return ['b' => $messages]; 54 | } 55 | ) 56 | ) 57 | ]), 58 | $translator 59 | ); 60 | 61 | $data = [ 62 | 'a' => 42, 63 | 'b' => 'gigi' 64 | ]; 65 | 66 | $errors = [ 67 | 'a' => ['Only strings here!'], 68 | 'b' => ['Only integers here!'] 69 | ]; 70 | 71 | expect($validator->validate($data)->equals(ValidationResult::errors($errors)))->toBeTruthy(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /spec/Example/ApplicativeMonadicSpec.php: -------------------------------------------------------------------------------- 1 | something 17 | * @return callable $a -> ($b -> something) 18 | */ 19 | function curry($f) 20 | { 21 | return function ($a) use ($f) { 22 | return function ($b) use ($a, $f) { 23 | return $f($a, $b); 24 | }; 25 | }; 26 | } 27 | 28 | /** 29 | * This example is replicated from https://fsharpforfunandprofit.com/posts/elevated-world-3/#validation 30 | */ 31 | 32 | class CustomerId implements Equality 33 | { 34 | /** 35 | * @var int should be positive 36 | */ 37 | private $id; 38 | 39 | public function __construct(int $id) 40 | { 41 | $this->id = $id; 42 | } 43 | 44 | /** 45 | * @param int $id 46 | * @return ValidationResult containing a CustomerId 47 | */ 48 | public static function buildValid(int $id): ValidationResult 49 | { 50 | return IsGreaterThan::withBound(0) 51 | ->validate($id) 52 | ->map(function (int $id) { 53 | return new self($id); 54 | }); 55 | } 56 | 57 | public function id(): int 58 | { 59 | return $this->id; 60 | } 61 | 62 | public function equals($that): bool 63 | { 64 | return $that instanceof self && $this->id === $that->id; 65 | } 66 | } 67 | 68 | // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses 69 | class EmailAddress implements Equality 70 | { 71 | /** 72 | * @var string should contain "@" 73 | */ 74 | private $email; 75 | 76 | public function __construct(string $email) 77 | { 78 | $this->email = $email; 79 | } 80 | 81 | /** 82 | * @param string $email 83 | * @return ValidationResult containing an EmailAddress 84 | */ 85 | public static function buildValid(string $email): ValidationResult 86 | { 87 | return Regex::withPattern('/^[\w.]+@[\w.]+$/u') 88 | ->validate($email) 89 | ->map(function (string $email) { 90 | return new self($email); 91 | }); 92 | } 93 | 94 | public function email(): string 95 | { 96 | return $this->email; 97 | } 98 | 99 | public function equals($that): bool 100 | { 101 | return $that instanceof self && $this->email === $that->email; 102 | } 103 | } 104 | 105 | // phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses 106 | class CustomerInfo implements Equality 107 | { 108 | /** 109 | * @var CustomerId 110 | */ 111 | private $id; 112 | 113 | /** 114 | * @var EmailAddress 115 | */ 116 | private $emailAddress; 117 | 118 | public function __construct( 119 | CustomerId $id, 120 | EmailAddress $emailAddress 121 | ) { 122 | $this->id = $id; 123 | $this->emailAddress = $emailAddress; 124 | } 125 | 126 | /** 127 | * @param int $id 128 | * @param string $email 129 | * @return ValidationResult containing a CustomerInfo 130 | */ 131 | public static function buildValidApplicative(int $id, string $email): ValidationResult 132 | { 133 | $idResult = CustomerId::buildValid($id); 134 | $emailResult = EmailAddress::buildValid($email); 135 | 136 | return $emailResult->apply($idResult->map(curry(function (CustomerId $id, EmailAddress $emailAddress) { 137 | return new self($id, $emailAddress); 138 | }))); 139 | } 140 | 141 | /** 142 | * we can rewrite buildValidApplicative using the lift function 143 | * 144 | * @param int $id 145 | * @param string $email 146 | * @return ValidationResult containing a CustomerInfo 147 | */ 148 | public static function buildValidApplicativeWithLift(int $id, string $email): ValidationResult 149 | { 150 | $idResult = CustomerId::buildValid($id); 151 | $emailResult = EmailAddress::buildValid($email); 152 | 153 | return lift(static function (CustomerId $id, EmailAddress $emailAddress) { 154 | return new self($id, $emailAddress); 155 | })($idResult, $emailResult); 156 | } 157 | 158 | /** 159 | * @param int $id 160 | * @param string $email 161 | * @return ValidationResult containing a CustomerInfo 162 | */ 163 | public static function buildValidMonadic(int $id, string $email): ValidationResult 164 | { 165 | $idResult = CustomerId::buildValid($id); 166 | $emailResult = EmailAddress::buildValid($email); 167 | 168 | return $idResult->bind(function (CustomerId $id) use ($emailResult) { 169 | return $emailResult->bind(function (EmailAddress $email) use ($id) { 170 | return ValidationResult::valid(new self($id, $email)); 171 | }); 172 | }); 173 | } 174 | 175 | /** 176 | * we can rewrite tbuildValidMonadic using the do_ function 177 | * 178 | * @param int $id 179 | * @param string $email 180 | * @return ValidationResult 181 | */ 182 | public static function buildValidMonadicWithDo(int $id, string $email): ValidationResult 183 | { 184 | return mdo( 185 | static function () use ($id) { 186 | return CustomerId::buildValid($id); 187 | }, 188 | static function () use ($email) { 189 | return EmailAddress::buildValid($email); 190 | }, 191 | static function ($id, $email) { 192 | return ValidationResult::valid(new self($id, $email)); 193 | } 194 | ); 195 | } 196 | 197 | public function id(): CustomerId 198 | { 199 | return $this->id; 200 | } 201 | 202 | public function emailAddress(): EmailAddress 203 | { 204 | return $this->emailAddress; 205 | } 206 | 207 | public function equals($that): bool 208 | { 209 | return $that instanceof self && 210 | $this->id->equals($that->id) && 211 | $this->emailAddress->equals($that->emailAddress); 212 | } 213 | } 214 | 215 | describe('applicative style', function () { 216 | it('validates correctly a customer with correct id and email', function () { 217 | expect( 218 | CustomerInfo::buildValidApplicative(42, 'gigi@zucon.it')->equals( 219 | ValidationResult::valid(new CustomerInfo(new CustomerId(42), new EmailAddress('gigi@zucon.it'))) 220 | ) 221 | )->toBeTruthy(); 222 | }); 223 | 224 | it('returns the correct error if the id is negative', function () { 225 | expect( 226 | CustomerInfo::buildValidApplicative(-42, 'gigi@zucon.it')->equals( 227 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 228 | ) 229 | )->toBeTruthy(); 230 | }); 231 | 232 | it('returns the correct error if the email is not valid', function () { 233 | expect( 234 | CustomerInfo::buildValidApplicative(42, 'gigi')->equals( 235 | ValidationResult::errors([Regex::MESSAGE]) 236 | ) 237 | )->toBeTruthy(); 238 | }); 239 | 240 | it('returns the correct error messages if both is and email are not valid', function () { 241 | expect( 242 | CustomerInfo::buildValidApplicative(-42, 'gigi')->equals( 243 | ValidationResult::errors([IsGreaterThan::MESSAGE, Regex::MESSAGE]) 244 | ) 245 | )->toBeTruthy(); 246 | }); 247 | }); 248 | 249 | describe('applicative style with lift', function () { 250 | it('validates correctly a customer with correct id and email', function () { 251 | expect( 252 | CustomerInfo::buildValidApplicativeWithLift(42, 'gigi@zucon.it')->equals( 253 | ValidationResult::valid(new CustomerInfo(new CustomerId(42), new EmailAddress('gigi@zucon.it'))) 254 | ) 255 | )->toBeTruthy(); 256 | }); 257 | 258 | it('returns the correct error if the id is negative', function () { 259 | expect( 260 | CustomerInfo::buildValidApplicativeWithLift(-42, 'gigi@zucon.it')->equals( 261 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 262 | ) 263 | )->toBeTruthy(); 264 | }); 265 | 266 | it('returns the correct error if the email is not valid', function () { 267 | expect( 268 | CustomerInfo::buildValidApplicativeWithLift(42, 'gigi')->equals( 269 | ValidationResult::errors([Regex::MESSAGE]) 270 | ) 271 | )->toBeTruthy(); 272 | }); 273 | 274 | it('returns the correct error messages if both is and email are not valid', function () { 275 | expect( 276 | CustomerInfo::buildValidApplicativeWithLift(-42, 'gigi')->equals( 277 | ValidationResult::errors([IsGreaterThan::MESSAGE, Regex::MESSAGE]) 278 | ) 279 | )->toBeTruthy(); 280 | }); 281 | }); 282 | 283 | describe('monadic style', function () { 284 | it('validates correctly a customer with correct id and email', function () { 285 | expect( 286 | CustomerInfo::buildValidMonadic(42, 'gigi@zucon.it')->equals( 287 | ValidationResult::valid(new CustomerInfo(new CustomerId(42), new EmailAddress('gigi@zucon.it'))) 288 | ) 289 | )->toBeTruthy(); 290 | }); 291 | 292 | it('returns the correct error if the id is negative', function () { 293 | expect( 294 | CustomerInfo::buildValidMonadic(-42, 'gigi@zucon.it')->equals( 295 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 296 | ) 297 | )->toBeTruthy(); 298 | }); 299 | 300 | it('returns the correct error if the email is not valid', function () { 301 | expect( 302 | CustomerInfo::buildValidMonadic(42, 'gigi')->equals( 303 | ValidationResult::errors([Regex::MESSAGE]) 304 | ) 305 | )->toBeTruthy(); 306 | }); 307 | 308 | it('returns the correct error messages if both is and email are not valid', function () { 309 | expect( 310 | CustomerInfo::buildValidMonadic(-42, 'gigi')->equals( 311 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 312 | ) 313 | )->toBeTruthy(); 314 | }); 315 | }); 316 | 317 | describe('monadic style with do_', function () { 318 | it('validates correctly a customer with correct id and email', function () { 319 | expect( 320 | CustomerInfo::buildValidMonadicWithDo(42, 'gigi@zucon.it')->equals( 321 | ValidationResult::valid(new CustomerInfo(new CustomerId(42), new EmailAddress('gigi@zucon.it'))) 322 | ) 323 | )->toBeTruthy(); 324 | }); 325 | 326 | it('returns the correct error if the id is negative', function () { 327 | expect( 328 | CustomerInfo::buildValidMonadicWithDo(-42, 'gigi@zucon.it')->equals( 329 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 330 | ) 331 | )->toBeTruthy(); 332 | }); 333 | 334 | it('returns the correct error if the email is not valid', function () { 335 | expect( 336 | CustomerInfo::buildValidMonadicWithDo(42, 'gigi')->equals( 337 | ValidationResult::errors([Regex::MESSAGE]) 338 | ) 339 | )->toBeTruthy(); 340 | }); 341 | 342 | it('returns the correct error messages if both is and email are not valid', function () { 343 | expect( 344 | CustomerInfo::buildValidMonadicWithDo(-42, 'gigi')->equals( 345 | ValidationResult::errors([IsGreaterThan::MESSAGE]) 346 | ) 347 | )->toBeTruthy(); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /spec/Result/ValidationResultSpec.php: -------------------------------------------------------------------------------- 1 | join($result2, $joinValid, 'array_merge'))->toEqual(ValidationResult::valid('gigibepi')); 20 | }); 21 | 22 | it('joins a valid result with an invalid one to an invalid result preserving errors', function () { 23 | $result1 = ValidationResult::valid('gigi'); 24 | $result2 = ValidationResult::errors(['bepi']); 25 | 26 | $joinValid = function ($a, $b) { 27 | return $a . $b; 28 | }; 29 | 30 | expect($result1->join($result2, $joinValid, 'array_merge'))->toEqual(ValidationResult::errors(['bepi'])); 31 | }); 32 | 33 | it('joins an invalid result with a valid one to an invalid result preserving errors', function () { 34 | $result1 = ValidationResult::errors(['gigi']); 35 | $result2 = ValidationResult::valid('bepi'); 36 | 37 | $joinValid = function ($a, $b) { 38 | return $a . $b; 39 | }; 40 | 41 | expect($result1->join($result2, $joinValid, 'array_merge'))->toEqual(ValidationResult::errors(['gigi'])); 42 | }); 43 | 44 | it('joins two invalid results to an invalid result merging errors', function () { 45 | $result1 = ValidationResult::errors(['gigi']); 46 | $result2 = ValidationResult::errors(['bepi']); 47 | 48 | $joinValid = function ($a, $b) { 49 | return $a . $b; 50 | }; 51 | 52 | expect($result1->join($result2, $joinValid, 'array_merge')) 53 | ->toEqual(ValidationResult::errors(['gigi', 'bepi'])); 54 | }); 55 | 56 | it('meets two valid result to a valid result joining the results', function () { 57 | $result1 = ValidationResult::valid('gigi'); 58 | $result2 = ValidationResult::valid('bepi'); 59 | 60 | expect($result1->meet( 61 | $result2, 62 | function ($x, $y) { 63 | return $x . $y; 64 | }, 65 | function ($x) { 66 | return $x; 67 | }, 68 | function ($y) { 69 | return $y; 70 | }, 71 | 'array_merge' 72 | ))->toEqual(ValidationResult::valid('gigibepi')); 73 | }); 74 | 75 | it('meets a valid result with an invalid one to an valid result preserving value', function () { 76 | $result1 = ValidationResult::valid('gigi'); 77 | $result2 = ValidationResult::errors(['bepi']); 78 | 79 | expect($result1->meet( 80 | $result2, 81 | function ($x, $y) { 82 | return $x . $y; 83 | }, 84 | function ($x) { 85 | return $x; 86 | }, 87 | function ($y) { 88 | return $y; 89 | }, 90 | 'array_merge' 91 | ))->toEqual(ValidationResult::valid('gigi')); 92 | }); 93 | 94 | it('meets an invalid result with a valid one to a valid result preserving value', function () { 95 | $result1 = ValidationResult::errors(['gigi']); 96 | $result2 = ValidationResult::valid('bepi'); 97 | 98 | expect($result1->meet( 99 | $result2, 100 | function ($x, $y) { 101 | return $x . $y; 102 | }, 103 | function ($x) { 104 | return $x; 105 | }, 106 | function ($y) { 107 | return $y; 108 | }, 109 | 'array_merge' 110 | ))->toEqual(ValidationResult::valid('bepi')); 111 | }); 112 | 113 | it('meets two invalid results to an invalid result merging errors', function () { 114 | $result1 = ValidationResult::errors(['gigi']); 115 | $result2 = ValidationResult::errors(['bepi']); 116 | 117 | expect($result1->meet( 118 | $result2, 119 | function ($x, $y) { 120 | return $x . $y; 121 | }, 122 | function ($x) { 123 | return $x; 124 | }, 125 | function ($y) { 126 | return $y; 127 | }, 128 | 'array_merge' 129 | ))->toEqual(ValidationResult::errors(['gigi', 'bepi'])); 130 | }); 131 | 132 | it('processes correctly a valid result', function () { 133 | $result = ValidationResult::valid(42); 134 | 135 | $f = function ($n) { 136 | return $n + 1; 137 | }; 138 | 139 | $id = function ($n) { 140 | return $n; 141 | }; 142 | 143 | expect($result->process($f, $id))->toEqual(43); 144 | }); 145 | 146 | it('processes correctly an invalid result', function () { 147 | $result = ValidationResult::errors(['gigi']); 148 | 149 | $id = function ($n) { 150 | return $n; 151 | }; 152 | 153 | $arrayUp = function ($a) { 154 | return array_map('strtoupper', $a); 155 | }; 156 | 157 | expect($result->process($id, $arrayUp))->toEqual(['GIGI']); 158 | }); 159 | 160 | it('maps a valid result to a valid result with a mapped value', function () { 161 | $result = ValidationResult::valid(42); 162 | 163 | $f = function ($n) { 164 | return $n + 1; 165 | }; 166 | 167 | expect($result->map($f))->toEqual(ValidationResult::valid(43)); 168 | }); 169 | 170 | it('maps an invalid result to itself', function () { 171 | $result = ValidationResult::errors(['gigi']); 172 | 173 | $f = function ($n) { 174 | return $n + 1; 175 | }; 176 | 177 | expect($result->map($f))->toEqual($result); 178 | }); 179 | 180 | it('mapErrors a valid result to itself', function () { 181 | $result = ValidationResult::valid(42); 182 | 183 | $arrayUp = function ($a) { 184 | return array_map('strtoupper', $a); 185 | }; 186 | 187 | expect($result->mapErrors($arrayUp))->toEqual($result); 188 | }); 189 | 190 | it('mapErrors an invalid result to an invalid result with mapped messages', function () { 191 | $result = ValidationResult::errors(['gigi']); 192 | 193 | $arrayUp = function ($a) { 194 | return array_map('strtoupper', $a); 195 | }; 196 | 197 | expect($result->mapErrors($arrayUp))->toEqual(ValidationResult::errors(['GIGI'])); 198 | }); 199 | 200 | it('binds correctly a valid result', function () { 201 | $result = ValidationResult::valid(42); 202 | 203 | $bind = function (int $n) { 204 | return IsAsAsserted::withAssertion(function (int $m) use ($n) { 205 | return $m < $n; 206 | })->validate(37); 207 | }; 208 | 209 | expect($result->bind($bind))->toEqual(ValidationResult::valid(37)); 210 | }); 211 | 212 | it('binds correctly an invalid result', function () { 213 | $result = ValidationResult::errors(['gigi']); 214 | 215 | $bind = function (int $n) { 216 | return IsAsAsserted::withAssertion(function (int $m) use ($n) { 217 | return $m < $n; 218 | })->validate(37); 219 | }; 220 | 221 | expect($result->bind($bind))->toEqual($result); 222 | }); 223 | 224 | it('applies correctly a valid function to a valid result', function () { 225 | $result = ValidationResult::valid(42); 226 | 227 | $apply = ValidationResult::valid(function (int $x) { 228 | return $x + 3; 229 | }); 230 | 231 | expect($result->apply($apply))->toEqual(ValidationResult::valid(45)); 232 | }); 233 | 234 | it('applies a valid function to an invalid result producing the same invalid result', function () { 235 | $result = ValidationResult::errors(['gigi']); 236 | 237 | $apply = ValidationResult::valid(function (int $x) { 238 | return $x + 3; 239 | }); 240 | 241 | expect($result->apply($apply))->toEqual($result); 242 | }); 243 | 244 | it('applies an invalid function to a valid result producing an invalid result', function () { 245 | $result = ValidationResult::valid(42); 246 | 247 | $apply = ValidationResult::errors(['gigi']); 248 | 249 | expect($result->apply($apply))->toEqual($apply); 250 | }); 251 | 252 | it('applies an invalid function to an invalid result producing an invalid result with both error messages', function () { 253 | $result = ValidationResult::errors(['gigi']); 254 | 255 | $apply = ValidationResult::errors(['toni']); 256 | 257 | expect($result->apply($apply))->toEqual(ValidationResult::errors(['toni', 'gigi'])); 258 | }); 259 | 260 | it('is equal to another result with the same valid content', function () { 261 | $result1 = ValidationResult::valid(42); 262 | $result2 = ValidationResult::valid(42); 263 | 264 | expect($result1->equals($result2))->toBeTruthy(); 265 | }); 266 | 267 | it('is equal to another invalid result with the same error message', function () { 268 | $result1 = ValidationResult::errors(['gigi']); 269 | $result2 = ValidationResult::errors(['gigi']); 270 | 271 | expect($result1->equals($result2))->toBeTruthy(); 272 | }); 273 | 274 | it('is not equal to an invalid result if valid', function () { 275 | $result1 = ValidationResult::valid(42); 276 | $result2 = ValidationResult::errors(['gigi']); 277 | 278 | expect($result1->equals($result2))->toBeFalsy(); 279 | }); 280 | 281 | it('is not equal to a valid result if invalid', function () { 282 | $result1 = ValidationResult::errors(['gigi']); 283 | $result2 = ValidationResult::valid(42); 284 | 285 | expect($result1->equals($result2))->toBeFalsy(); 286 | }); 287 | }); 288 | -------------------------------------------------------------------------------- /spec/Result/functionsSpec.php: -------------------------------------------------------------------------------- 1 | equals(ValidationResult::valid(42)))->toBeTruthy(); 20 | }); 21 | 22 | it('lifts a function with one argument, handling the valid case', function () { 23 | $f = static function (int $a) { 24 | return $a + 3; 25 | }; 26 | 27 | expect((lift($f)(ValidationResult::valid(42)))->equals(ValidationResult::valid(45)))->toBeTruthy(); 28 | }); 29 | 30 | it('lifts a function with one argument, handling the failure case', function () { 31 | $f = static function (int $a) { 32 | return $a + 3; 33 | }; 34 | 35 | expect( 36 | (lift($f)(ValidationResult::errors(['nope'])))->equals(ValidationResult::errors(['nope'])) 37 | )->toBeTruthy(); 38 | }); 39 | 40 | it('lifts a function with two arguments, handling the valid case', function () { 41 | $f = static function ($a, $b) { 42 | return $a + $b; 43 | }; 44 | 45 | expect( 46 | (lift($f)(ValidationResult::valid(42), ValidationResult::valid(23)))->equals(ValidationResult::valid(65)) 47 | )->toBeTruthy(); 48 | }); 49 | 50 | it('lifts a function with two arguments, handling the failure of the first argument', function () { 51 | $f = static function ($a, $b) { 52 | return $a + $b; 53 | }; 54 | 55 | expect( 56 | (lift($f)(ValidationResult::errors(['nope']), ValidationResult::valid(23))) 57 | ->equals(ValidationResult::errors(['nope'])) 58 | )->toBeTruthy(); 59 | }); 60 | 61 | it('lifts a function with two arguments, handling the failure of the second argument', function () { 62 | $f = static function ($a, $b) { 63 | return $a + $b; 64 | }; 65 | 66 | expect( 67 | (lift($f)(ValidationResult::valid(23), ValidationResult::errors(['nope']))) 68 | ->equals(ValidationResult::errors(['nope'])) 69 | )->toBeTruthy(); 70 | }); 71 | 72 | it('lifts a function with two arguments, handling the failure of both arguments', function () { 73 | $f = static function ($a, $b) { 74 | return $a + $b; 75 | }; 76 | 77 | expect( 78 | (lift($f)(ValidationResult::errors(['nope1']), ValidationResult::errors(['nope2']))) 79 | ->equals(ValidationResult::errors(['nope1', 'nope2'])) 80 | )->toBeTruthy(); 81 | }); 82 | 83 | it('lifts a function with three arguments, handling the valid case', function () { 84 | $f = static function ($a, $b, $c) { 85 | return $a + $b + $c; 86 | }; 87 | 88 | expect( 89 | (lift($f)(ValidationResult::valid(42), ValidationResult::valid(23), ValidationResult::valid(67))) 90 | ->equals(ValidationResult::valid(132)) 91 | )->toBeTruthy(); 92 | }); 93 | 94 | it('lifts a function with three arguments, handling the failure of the first argument', function () { 95 | $f = static function ($a, $b, $c) { 96 | return $a + $b + $c; 97 | }; 98 | 99 | expect( 100 | (lift($f)(ValidationResult::errors(['nope']), ValidationResult::valid(23), ValidationResult::valid(67))) 101 | ->equals(ValidationResult::errors(['nope'])) 102 | )->toBeTruthy(); 103 | }); 104 | 105 | it('lifts a function with three arguments, handling the failure of the second argument', function () { 106 | $f = static function ($a, $b, $c) { 107 | return $a + $b + $c; 108 | }; 109 | 110 | expect( 111 | (lift($f)(ValidationResult::valid(42), ValidationResult::errors(['nope']), ValidationResult::valid(67))) 112 | ->equals(ValidationResult::errors(['nope'])) 113 | )->toBeTruthy(); 114 | }); 115 | 116 | it('lifts a function with three arguments, handling the failure of the third argument', function () { 117 | $f = static function ($a, $b, $c) { 118 | return $a + $b + $c; 119 | }; 120 | 121 | expect( 122 | (lift($f)(ValidationResult::valid(42), ValidationResult::valid(23), ValidationResult::errors(['nope']))) 123 | ->equals(ValidationResult::errors(['nope'])) 124 | )->toBeTruthy(); 125 | }); 126 | 127 | it('lifts a function with three arguments, handling the failure of the first and second argument', function () { 128 | $f = static function ($a, $b, $c) { 129 | return $a + $b + $c; 130 | }; 131 | 132 | expect( 133 | (lift($f)(ValidationResult::errors(['nope1']), ValidationResult::errors(['nope2']), ValidationResult::valid(67))) 134 | ->equals(ValidationResult::errors(['nope1', 'nope2'])) 135 | )->toBeTruthy(); 136 | }); 137 | 138 | it('lifts a function with three arguments, handling the failure of the first and third argument', function () { 139 | $f = static function ($a, $b, $c) { 140 | return $a + $b + $c; 141 | }; 142 | 143 | expect( 144 | (lift($f)(ValidationResult::errors(['nope1']), ValidationResult::valid(23), ValidationResult::errors(['nope3']))) 145 | ->equals(ValidationResult::errors(['nope1', 'nope3'])) 146 | )->toBeTruthy(); 147 | }); 148 | 149 | it('lifts a function with three arguments, handling the failure of the second and third argument', function () { 150 | $f = static function ($a, $b, $c) { 151 | return $a + $b + $c; 152 | }; 153 | 154 | expect( 155 | (lift($f)(ValidationResult::valid(42), ValidationResult::errors(['nope2']), ValidationResult::errors(['nope3']))) 156 | ->equals(ValidationResult::errors(['nope2', 'nope3'])) 157 | )->toBeTruthy(); 158 | }); 159 | 160 | it('lifts a function with three arguments, handling the failure of all arguments', function () { 161 | $f = static function ($a, $b, $c) { 162 | return $a + $b + $c; 163 | }; 164 | 165 | expect( 166 | (lift($f)(ValidationResult::errors(['nope1']), ValidationResult::errors(['nope2']), ValidationResult::errors(['nope3']))) 167 | ->equals(ValidationResult::errors(['nope1', 'nope2', 'nope3'])) 168 | )->toBeTruthy(); 169 | }); 170 | }); 171 | 172 | describe('do_ function', function () { 173 | it('sums two numbers', function () { 174 | $sumResult = sdo( 175 | static function () { 176 | return ValidationResult::valid(42); 177 | }, 178 | static function ($arg) { 179 | return ValidationResult::valid(['first' => $arg, 'second' => 23]); 180 | }, 181 | static function ($args) { 182 | return ValidationResult::valid($args['first'] + $args['second']); 183 | } 184 | ); 185 | 186 | expect($sumResult->equals(ValidationResult::valid(65)))->toBeTruthy(); 187 | }); 188 | 189 | it('fails if the first operation fails', function () { 190 | $sumResult = sdo( 191 | static function () { 192 | return ValidationResult::errors(['nope']); 193 | }, 194 | static function ($arg) { 195 | return ValidationResult::valid(['first' => $arg, 'second' => 23]); 196 | }, 197 | static function ($args) { 198 | return ValidationResult::valid($args['first'] + $args['second']); 199 | } 200 | ); 201 | 202 | expect($sumResult->equals(ValidationResult::errors(['nope'])))->toBeTruthy(); 203 | }); 204 | 205 | it('fails if the second operation fails', function () { 206 | $sumResult = sdo( 207 | static function () { 208 | return ValidationResult::valid(42); 209 | }, 210 | static function () { 211 | return ValidationResult::errors(['nope']); 212 | }, 213 | static function ($args) { 214 | return ValidationResult::valid($args['first'] + $args['second']); 215 | } 216 | ); 217 | 218 | expect($sumResult->equals(ValidationResult::errors(['nope'])))->toBeTruthy(); 219 | }); 220 | 221 | it('fails if both operation fails with just the first error', function () { 222 | $sumResult = sdo( 223 | static function () { 224 | return ValidationResult::errors(['nope1']); 225 | }, 226 | static function () { 227 | return ValidationResult::errors(['nope2']); 228 | }, 229 | static function ($args) { 230 | return ValidationResult::valid($args['first'] + $args['second']); 231 | } 232 | ); 233 | 234 | expect($sumResult->equals(ValidationResult::errors(['nope1'])))->toBeTruthy(); 235 | }); 236 | }); 237 | 238 | describe('do__ function', function () { 239 | it('sums two numbers', function () { 240 | $sumResult = mdo( 241 | static function () { 242 | return ValidationResult::valid(42); 243 | }, 244 | static function () { 245 | return ValidationResult::valid(23); 246 | }, 247 | static function ($arg1, $arg2) { 248 | return ValidationResult::valid($arg1 + $arg2); 249 | } 250 | ); 251 | 252 | expect($sumResult->equals(ValidationResult::valid(65)))->toBeTruthy(); 253 | }); 254 | 255 | it('fails if the first operation fails', function () { 256 | $sumResult = mdo( 257 | static function () { 258 | return ValidationResult::errors(['nope']); 259 | }, 260 | static function () { 261 | return ValidationResult::valid(23); 262 | }, 263 | static function ($arg1, $arg2) { 264 | return ValidationResult::valid($arg1 + $arg2); 265 | } 266 | ); 267 | 268 | expect($sumResult->equals(ValidationResult::errors(['nope'])))->toBeTruthy(); 269 | }); 270 | 271 | it('fails if the second operation fails', function () { 272 | $sumResult = mdo( 273 | static function () { 274 | return ValidationResult::valid(42); 275 | }, 276 | static function () { 277 | return ValidationResult::errors(['nope']); 278 | }, 279 | static function ($arg1, $arg2) { 280 | return ValidationResult::valid($arg1 + $arg2); 281 | } 282 | ); 283 | 284 | expect($sumResult->equals(ValidationResult::errors(['nope'])))->toBeTruthy(); 285 | }); 286 | 287 | it('fails if both operation fails with just the first error', function () { 288 | $sumResult = mdo( 289 | static function () { 290 | return ValidationResult::errors(['nope1']); 291 | }, 292 | static function () { 293 | return ValidationResult::errors(['nope2']); 294 | }, 295 | static function ($arg1, $arg2) { 296 | return ValidationResult::valid($arg1 + $arg2); 297 | } 298 | ); 299 | 300 | expect($sumResult->equals(ValidationResult::errors(['nope1'])))->toBeTruthy(); 301 | }); 302 | }); 303 | -------------------------------------------------------------------------------- /spec/Translator/Combinator/CoalesceSpec.php: -------------------------------------------------------------------------------- 1 | translate('gigi'))->toBe('gigi'); 15 | }); 16 | 17 | it('returns the received string if it receives only null translators', function () { 18 | $translator = Coalesce::withTranslators(null, null, null); 19 | 20 | expect($translator->translate('gigi'))->toBe('gigi'); 21 | }); 22 | 23 | it('returns the translated string according to the first non-null translator', function () { 24 | $translator1 = ConstantTranslator::withTranslation('bepi'); 25 | $translator2 = ConstantTranslator::withTranslation('toni'); 26 | $translator = Coalesce::withTranslators(null, $translator1, $translator2); 27 | 28 | expect($translator->translate('gigi'))->toBe('bepi'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /spec/Translator/ConstantTranslatorSpec.php: -------------------------------------------------------------------------------- 1 | translate('gigi'))->toBe('bepi'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /spec/Translator/IdentityTranslatorSpec.php: -------------------------------------------------------------------------------- 1 | translate('gigi'))->toBe('gigi'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /spec/Translator/KeyValueTranslatorSpec.php: -------------------------------------------------------------------------------- 1 | 'bepi']); 12 | 13 | expect($translator->translate('gigi'))->toBe('bepi'); 14 | }); 15 | 16 | it('returns the received string if it is not in the dictionary', function () { 17 | $translator = KeyValueTranslator::withDictionary(['gigi' => 'bepi']); 18 | 19 | expect($translator->translate('toni'))->toBe('toni'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /spec/functionsSpec.php: -------------------------------------------------------------------------------- 1 | toBe(42); 25 | }); 26 | 27 | it('does not modify a function with one argument', function () { 28 | $f = function (int $a) { 29 | return $a + 3; 30 | }; 31 | 32 | expect(curry($f)(2))->toBe(5); 33 | }); 34 | 35 | it('curries a function with two arguments', function () { 36 | $f = function (int $a, int $b) { 37 | return $a + $b; 38 | }; 39 | 40 | expect(curry($f)(2)(3))->toBe(5); 41 | }); 42 | 43 | it('curries a function with three arguments', function () { 44 | $f = function (int $a, int $b, int $c) { 45 | return $a + $b + $c; 46 | }; 47 | 48 | expect(curry($f)(2)(3)(4))->toBe(9); 49 | }); 50 | 51 | it('curries an object method', function () { 52 | $foo = new class { 53 | public function bar(int $a, int $b): int 54 | { 55 | return $a + $b; 56 | } 57 | }; 58 | 59 | $f = [$foo, 'bar']; 60 | 61 | expect(curry($f)(2)(3))->toBe(5); 62 | }); 63 | 64 | it('curries a static method', function () { 65 | $f = [Adder::class, 'sum']; 66 | 67 | expect(curry($f)(2)(3))->toBe(5); 68 | }); 69 | 70 | it('curries as invokable object', function () { 71 | $f = new class { 72 | public function __invoke(int $a, int $b): int 73 | { 74 | return $a + $b; 75 | } 76 | }; 77 | 78 | expect(curry($f)(2)(3))->toBe(5); 79 | }); 80 | }); 81 | 82 | describe('uncurry function', function () { 83 | it('does not modify a function with no arguments', function () { 84 | $f = function () { 85 | return 42; 86 | }; 87 | 88 | expect(uncurry($f)())->toBe(42); 89 | }); 90 | 91 | it('does not modify a function with one argument', function () { 92 | $f = function (int $a) { 93 | return $a + 3; 94 | }; 95 | 96 | expect(uncurry($f)(2))->toBe(5); 97 | }); 98 | 99 | it('uncurries a curried function with two arguments', function () { 100 | $f = function (int $a) { 101 | return function (int $b) use ($a) { 102 | return $a + $b; 103 | }; 104 | }; 105 | 106 | expect(uncurry($f)(2, 3))->toBe(5); 107 | }); 108 | 109 | it('uncurries a curried function with three arguments', function () { 110 | $f = function (int $a) { 111 | return function ($b) use ($a) { 112 | return function (int $c) use ($a, $b) { 113 | return $a + $b + $c; 114 | }; 115 | }; 116 | }; 117 | 118 | expect(uncurry($f)(2, 3, 4))->toBe(9); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/Basic/Compare.php: -------------------------------------------------------------------------------- 1 | comparisonBasis = $comparisonBasis; 31 | $this->errorFormatter = $errorFormatter; 32 | } 33 | 34 | /** 35 | * @template B 36 | * @param B $comparisonBasis 37 | * @return self 38 | */ 39 | public static function withBound($comparisonBasis): self 40 | { 41 | return self::withBoundAndFormatter( 42 | $comparisonBasis, 43 | /** 44 | * @param B $comparisonBasis 45 | * @param B $data 46 | * @return string[] 47 | */ 48 | function ($comparisonBasis, $data): array { 49 | /** @var string $message */ 50 | $message = static::MESSAGE; 51 | 52 | return [$message]; 53 | } 54 | ); 55 | } 56 | 57 | /** 58 | * @template B 59 | * @template F 60 | * @param B $comparisonBasis 61 | * @param callable(B, B): F[] $errorFormatter 62 | * @return self 63 | */ 64 | public static function withBoundAndFormatter($comparisonBasis, callable $errorFormatter): self 65 | { 66 | /** @psalm-suppress UnsafeInstantiation */ 67 | return new static($comparisonBasis, $errorFormatter); 68 | } 69 | 70 | /** 71 | * @template B 72 | * @param B $comparisonBasis 73 | * @param Translator $translator 74 | * @return Compare 75 | */ 76 | public static function withBoundAndTranslator($comparisonBasis, Translator $translator): self 77 | { 78 | return self::withBoundAndFormatter( 79 | $comparisonBasis, 80 | /** 81 | * @param B $comparisonBasis 82 | * @param B $data 83 | * @return string[] 84 | */ 85 | function ($comparisonBasis, $data) use ($translator): array { 86 | /** @var string $message */ 87 | $message = static::MESSAGE; 88 | 89 | return [$translator->translate($message)]; 90 | } 91 | ); 92 | } 93 | 94 | /** 95 | * @param A $data 96 | * @return ValidationResult 97 | */ 98 | abstract public function validate($data, array $context = []): ValidationResult; 99 | 100 | /** 101 | * @param callable(A, A): bool $assertion 102 | * @param A $data 103 | * @return ValidationResult 104 | */ 105 | protected function validateAssertion(callable $assertion, $data, array $context = []): ValidationResult 106 | { 107 | $comparisonBasis = $this->comparisonBasis; 108 | $errorFormatter = $this->errorFormatter; 109 | 110 | return IsAsAsserted::withAssertionAndErrorFormatter( 111 | /** 112 | * @param A $data 113 | */ 114 | function ($data) use ($comparisonBasis, $assertion): bool { 115 | return $assertion($comparisonBasis, $data); 116 | }, 117 | /** 118 | * @param A $data 119 | */ 120 | function ($data) use ($comparisonBasis, $errorFormatter): array { 121 | return $errorFormatter($comparisonBasis, $data); 122 | } 123 | )->validate($data, $context); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Basic/ComposingAssertion.php: -------------------------------------------------------------------------------- 1 | errorFormatter = $errorFormatter; 29 | } 30 | 31 | /** 32 | * @template B 33 | * @template F 34 | * @param callable(B): F[] $errorFormatter 35 | * @return self 36 | */ 37 | public static function withFormatter(callable $errorFormatter): self 38 | { 39 | /** @psalm-suppress UnsafeInstantiation */ 40 | return new static($errorFormatter); 41 | } 42 | 43 | /** 44 | * @template B 45 | * @return self 46 | */ 47 | public static function withTranslator(Translator $translator): self 48 | { 49 | /** @psalm-suppress UnsafeInstantiation */ 50 | return new static( 51 | /** 52 | * @param B $data 53 | * @return string[] 54 | */ 55 | function ($data) use ($translator): array { 56 | /** @var string $message */ 57 | $message = static::MESSAGE; 58 | 59 | return [$translator->translate($message)]; 60 | } 61 | ); 62 | } 63 | 64 | /** 65 | * @param A $data 66 | * @return ValidationResult 67 | */ 68 | abstract public function validate($data, array $context = []): ValidationResult; 69 | 70 | /** 71 | * @param callable(A): bool $assertion 72 | * @param A $data 73 | * @return ValidationResult 74 | */ 75 | protected function validateAssertion(callable $assertion, $data, array $context = []): ValidationResult 76 | { 77 | return IsAsAsserted::withAssertionAndErrorFormatter( 78 | $assertion, 79 | is_callable($this->errorFormatter) ? 80 | $this->errorFormatter : 81 | /** 82 | * @param A $data 83 | * @return string[] 84 | */ 85 | function ($data): array { 86 | /** @var string $message */ 87 | $message = static::MESSAGE; 88 | 89 | return [$message]; 90 | } 91 | )->validate($data, $context); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Basic/Errors.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class Errors extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'errors.invalid-data'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | $alwaysFalse = 27 | /** 28 | * @param A $data 29 | * @return false 30 | */ 31 | function ($data): bool { 32 | return false; 33 | }; 34 | 35 | return $this->validateAssertion($alwaysFalse, $data, $context); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Basic/HasKey.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class HasKey implements Validation 20 | { 21 | public const MISSING_KEY = 'has-key.missing-key'; 22 | 23 | /** @var array-key */ 24 | private $key; 25 | 26 | /** @var callable(array-key, A): E[] */ 27 | private $errorFormatter; 28 | 29 | /** 30 | * @param array-key $key 31 | * @param null|callable(array-key, A): E[] $errorFormatter 32 | */ 33 | private function __construct($key, ?callable $errorFormatter = null) 34 | { 35 | $this->key = $key; 36 | 37 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 38 | $this->errorFormatter = is_callable($errorFormatter) ? 39 | $errorFormatter : 40 | /** 41 | * @param array-key $key 42 | * @param A $data 43 | * @return string[] 44 | */ 45 | function ($key, array $data): array { 46 | return [self::MISSING_KEY]; 47 | }; 48 | } 49 | 50 | /** 51 | * @param array-key $key 52 | */ 53 | public static function withKey(string $key): self 54 | { 55 | return new self($key); 56 | } 57 | 58 | /** 59 | * @template F 60 | * @param array-key $key 61 | * @param callable(array-key, array): F[] $errorFormatter 62 | * @return self 63 | */ 64 | public static function withKeyAndFormatter($key, callable $errorFormatter): self 65 | { 66 | return new self($key, $errorFormatter); 67 | } 68 | 69 | /** 70 | * @param array-key $key 71 | * @param Translator $translator 72 | * @return self 73 | * @psalm-suppress MixedReturnTypeCoercion 74 | */ 75 | public static function withKeyAndTranslator($key, Translator $translator): self 76 | { 77 | return new self( 78 | $key, 79 | /** 80 | * @param array-key $key 81 | * @param array $data 82 | * @return string[] 83 | */ 84 | function ($key, array $data) use ($translator): array { 85 | return [$translator->translate(self::MISSING_KEY)]; 86 | } 87 | ); 88 | } 89 | 90 | /** 91 | * @param A $data 92 | * @return ValidationResult 93 | * @psalm-suppress MoreSpecificImplementedParamType 94 | */ 95 | public function validate($data, array $context = []): ValidationResult 96 | { 97 | if (! array_key_exists($this->key, $data)) { 98 | /** @var ValidationResult $ret */ 99 | $ret = ValidationResult::errors(($this->errorFormatter)($this->key, $data)); 100 | 101 | return $ret; 102 | } 103 | 104 | return ValidationResult::valid($data); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Basic/HasNotKey.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class HasNotKey implements Validation 19 | { 20 | public const PRESENT_KEY = 'has-not-key.present-key'; 21 | 22 | /** @var array-key */ 23 | private $key; 24 | 25 | /** @var callable(array-key, A): E[] */ 26 | private $errorFormatter; 27 | 28 | /** 29 | * @param array-key $key 30 | * @param null|callable(array-key, A): E[] $errorFormatter 31 | */ 32 | private function __construct($key, ?callable $errorFormatter = null) 33 | { 34 | $this->key = $key; 35 | 36 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 37 | $this->errorFormatter = is_callable($errorFormatter) ? 38 | $errorFormatter : 39 | /** 40 | * @param array-key $key 41 | * @param A $data 42 | * @return string[] 43 | */ 44 | function (string $key, $data): array { 45 | return [self::PRESENT_KEY]; 46 | }; 47 | } 48 | 49 | /** 50 | * @param array-key $key 51 | */ 52 | public static function withKey(string $key): self 53 | { 54 | return new self($key); 55 | } 56 | 57 | /** 58 | * @template B of array 59 | * @param array-key $key 60 | * @param callable(array-key, B): E[] $errorFormatter 61 | */ 62 | public static function withKeyAndFormatter(string $key, callable $errorFormatter): self 63 | { 64 | return new self($key, $errorFormatter); 65 | } 66 | 67 | /** 68 | * @param array-key $key 69 | * @param Translator $translator 70 | * @return self 71 | * @psalm-suppress MixedReturnTypeCoercion 72 | */ 73 | public static function withKeyAndTranslator($key, Translator $translator): self 74 | { 75 | return new self( 76 | $key, 77 | /** 78 | * @param array-key $key 79 | * @param array $data 80 | * @return string[] 81 | */ 82 | function (string $key, $data) use ($translator): array { 83 | return [$translator->translate(self::PRESENT_KEY)]; 84 | } 85 | ); 86 | } 87 | 88 | /** 89 | * @param A $data 90 | * @return ValidationResult 91 | * @psalm-suppress MoreSpecificImplementedParamType 92 | */ 93 | public function validate($data, array $context = []): ValidationResult 94 | { 95 | if (array_key_exists($this->key, $data)) { 96 | /** @var ValidationResult $ret */ 97 | $ret = ValidationResult::errors(($this->errorFormatter)($this->key, $data)); 98 | 99 | return $ret; 100 | } 101 | 102 | return ValidationResult::valid($data); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Basic/InArray.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class InArray implements Validation 20 | { 21 | public const NOT_IN_ARRAY = 'in-array.not-in-array'; 22 | 23 | /** @var array */ 24 | private $values; 25 | 26 | /** @var callable(array, A): E[] */ 27 | private $errorFormatter; 28 | 29 | /** 30 | * @param null|callable(array, A): E[] $errorFormatter 31 | */ 32 | private function __construct(array $values, ?callable $errorFormatter = null) 33 | { 34 | $this->values = $values; 35 | 36 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 37 | $this->errorFormatter = is_callable($errorFormatter) ? 38 | $errorFormatter : 39 | /** 40 | * @param A $data 41 | * @return string[] 42 | */ 43 | function (array $values, $data): array { 44 | return [self::NOT_IN_ARRAY]; 45 | }; 46 | } 47 | 48 | /** 49 | * @template B 50 | * @return self 51 | * @psalm-suppress MixedReturnTypeCoercion 52 | */ 53 | public static function withValues(array $values): self 54 | { 55 | return new self($values); 56 | } 57 | 58 | /** 59 | * @template F 60 | * @template B 61 | * @param callable(array, B): F[] $errorFormatter 62 | * @return self 63 | */ 64 | public static function withValuesAndFormatter(array $values, callable $errorFormatter): self 65 | { 66 | return new self($values, $errorFormatter); 67 | } 68 | 69 | /** 70 | * @template B 71 | * @param array $values 72 | * @return self 73 | * @psalm-suppress MixedReturnTypeCoercion 74 | */ 75 | public static function withValuesAndTranslator(array $values, Translator $translator): self 76 | { 77 | return new self( 78 | $values, 79 | /** 80 | * @param A $data 81 | * @return string[] 82 | */ 83 | function (array $values, $data) use ($translator): array { 84 | return [$translator->translate(self::NOT_IN_ARRAY)]; 85 | } 86 | ); 87 | } 88 | 89 | /** 90 | * @param A $data 91 | * @return ValidationResult 92 | */ 93 | public function validate($data, array $context = []): ValidationResult 94 | { 95 | if (! in_array($data, $this->values, true)) { 96 | return ValidationResult::errors(($this->errorFormatter)($this->values, $data)); 97 | } 98 | 99 | return ValidationResult::valid($data); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Basic/IsArray.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsArray extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-array.no-an-array'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_array', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsAsAsserted.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class IsAsAsserted implements Validation 19 | { 20 | public const NOT_AS_ASSERTED = 'is-as-asserted.not-as-asserted'; 21 | 22 | /** @var callable(A): bool */ 23 | private $assertion; 24 | 25 | /** @var callable(A): E[] */ 26 | private $errorFormatter; 27 | 28 | /** 29 | * @param callable(A): bool $assertion 30 | * @param null|callable(A): E[] $errorFormatter 31 | */ 32 | private function __construct(callable $assertion, ?callable $errorFormatter = null) 33 | { 34 | $this->assertion = $assertion; 35 | 36 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 37 | $this->errorFormatter = is_callable($errorFormatter) ? 38 | $errorFormatter : 39 | /** 40 | * @param A $data 41 | * @return string[] 42 | */ 43 | function ($data): array { 44 | return [self::NOT_AS_ASSERTED]; 45 | }; 46 | } 47 | 48 | /** 49 | * @template B 50 | * @param callable(B): bool $assertion 51 | * @return self 52 | * @psalm-suppress MixedReturnTypeCoercion 53 | */ 54 | public static function withAssertion(callable $assertion): self 55 | { 56 | return new self($assertion); 57 | } 58 | 59 | /** 60 | * @template B 61 | * @param callable(B): bool $assertion 62 | * @param callable(B): E[] $errorFormatter 63 | * @return self 64 | */ 65 | public static function withAssertionAndErrorFormatter(callable $assertion, callable $errorFormatter): self 66 | { 67 | return new self($assertion, $errorFormatter); 68 | } 69 | 70 | /** 71 | * @template B 72 | * @param callable(B): bool $assertion 73 | * @param Translator $translator 74 | * @return self 75 | * @psalm-suppress MixedReturnTypeCoercion 76 | */ 77 | public static function withAssertionAndTranslator(callable $assertion, Translator $translator): self 78 | { 79 | return new self( 80 | $assertion, 81 | /** 82 | * @param A $data 83 | * @return string[] 84 | */ 85 | function ($data) use ($translator): array { 86 | return [$translator->translate(self::NOT_AS_ASSERTED)]; 87 | } 88 | ); 89 | } 90 | 91 | /** 92 | * @param A $data 93 | * @return ValidationResult 94 | */ 95 | public function validate($data, array $context = []): ValidationResult 96 | { 97 | if (! ($this->assertion)($data)) { 98 | return ValidationResult::errors(($this->errorFormatter)($data)); 99 | } 100 | 101 | return ValidationResult::valid($data); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Basic/IsBool.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsBool extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-bool.not-a-bool'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_bool', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsCallable.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsCallable extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-callable.not-a-callable'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_callable', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsFloat.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsFloat extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-float.not-a-float'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_float', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsGreaterThan.php: -------------------------------------------------------------------------------- 1 | 16 | * @implements Validation 17 | */ 18 | final class IsGreaterThan extends Compare implements Validation 19 | { 20 | public const MESSAGE = 'is-greater-than.not-greater-than'; 21 | 22 | /** 23 | * @param A $data 24 | * @return ValidationResult 25 | */ 26 | public function validate($data, array $context = []): ValidationResult 27 | { 28 | return $this->validateAssertion( 29 | /** 30 | * @param A $bound 31 | * @param A $data 32 | */ 33 | function ($bound, $data): bool { 34 | return $data > $bound; 35 | }, 36 | $data, 37 | $context 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Basic/IsInstanceOf.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class IsInstanceOf implements Validation 19 | { 20 | public const NOT_AN_INSTANCE = 'is-instance-of.not-an-instance'; 21 | 22 | /** 23 | * @var class-string 24 | */ 25 | private $className; 26 | 27 | /** 28 | * @var callable(class-string, A): E[] 29 | */ 30 | private $errorFormatter; 31 | 32 | /** 33 | * @param class-string $className 34 | * @param null|callable(class-string, A): E[] $errorFormatter 35 | */ 36 | private function __construct(string $className, ?callable $errorFormatter = null) 37 | { 38 | $this->className = $className; 39 | 40 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 41 | $this->errorFormatter = is_callable($errorFormatter) ? 42 | $errorFormatter : 43 | /** 44 | * @param class-string $className 45 | * @param A $data 46 | * @return string[] 47 | */ 48 | function (string $className, $data): array { 49 | return [self::NOT_AN_INSTANCE]; 50 | }; 51 | } 52 | 53 | /** 54 | * @template B 55 | * @param class-string $className 56 | * @return self 57 | * @psalm-suppress MixedReturnTypeCoercion 58 | */ 59 | public static function withClassName(string $className): self 60 | { 61 | return new self($className); 62 | } 63 | 64 | /** 65 | * @template F 66 | * @template B 67 | * @param class-string $className 68 | * @param callable(class-string, B): F[] $errorFormatter 69 | * @return self 70 | */ 71 | public static function withClassNameAndFormatter(string $className, callable $errorFormatter): self 72 | { 73 | return new self($className, $errorFormatter); 74 | } 75 | 76 | /** 77 | * @template B 78 | * @param class-string $className 79 | * @param Translator $translator 80 | * @return self 81 | * @psalm-suppress MixedReturnTypeCoercion 82 | */ 83 | public static function withClassNameAndTranslator(string $className, Translator $translator): self 84 | { 85 | return new self( 86 | $className, 87 | /** 88 | * @param class-string $className 89 | * @param A $data 90 | * @return string[] 91 | */ 92 | function (string $className, $data) use ($translator): array { 93 | return [$translator->translate(self::NOT_AN_INSTANCE)]; 94 | } 95 | ); 96 | } 97 | 98 | /** 99 | * @param A $data 100 | * @return ValidationResult 101 | */ 102 | public function validate($data, array $context = []): ValidationResult 103 | { 104 | if (! $data instanceof $this->className) { 105 | return ValidationResult::errors(($this->errorFormatter)($this->className, $data)); 106 | } 107 | 108 | return ValidationResult::valid($data); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Basic/IsInteger.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsInteger extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-integer.not-an-integer'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_int', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsIterable.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsIterable extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-iterable.not-an-iterable'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_iterable', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsLessThan.php: -------------------------------------------------------------------------------- 1 | 15 | * @implements Validation 16 | */ 17 | final class IsLessThan extends Compare implements Validation 18 | { 19 | public const MESSAGE = 'is-less-than.not-less-than'; 20 | 21 | /** 22 | * @param A $data 23 | * @return ValidationResult 24 | */ 25 | public function validate($data, array $context = []): ValidationResult 26 | { 27 | return $this->validateAssertion( 28 | /** 29 | * @param A $bound 30 | * @param A $data 31 | */ 32 | function ($bound, $data): bool { 33 | return $data < $bound; 34 | }, 35 | $data, 36 | $context 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Basic/IsNotNull.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsNotNull extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-not-null.not-not-null'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion( 27 | /** 28 | * @param A $data 29 | */ 30 | function ($data): bool { 31 | return null !== $data; 32 | }, 33 | $data, 34 | $context 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Basic/IsNull.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsNull extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-null.not-null'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion( 27 | /** 28 | * @param A $data 29 | */ 30 | function ($data): bool { 31 | return null === $data; 32 | }, 33 | $data, 34 | $context 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Basic/IsNumeric.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsNumeric extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-numeric.not-numeric'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_numeric', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsObject.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsObject extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-object.not-an-object'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_object', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsResource.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsResource extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-resource.not-a-resource'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_resource', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/IsString.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class IsString extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'is-string.not-a-string'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion('is_string', $data, $context); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Basic/NonEmpty.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements Validation 15 | */ 16 | final class NonEmpty extends ComposingAssertion implements Validation 17 | { 18 | public const MESSAGE = 'non-empty.empty'; 19 | 20 | /** 21 | * @param A $data 22 | * @return ValidationResult 23 | */ 24 | public function validate($data, array $context = []): ValidationResult 25 | { 26 | return $this->validateAssertion( 27 | /** 28 | * @param A $data 29 | */ 30 | function ($data): bool { 31 | return !empty($data); 32 | }, 33 | $data, 34 | $context 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Basic/Regex.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class Regex implements Validation 19 | { 20 | public const MESSAGE = 'regex.match-failed'; 21 | 22 | /** @var string */ 23 | private $pattern; 24 | 25 | /** @var callable(string, string): E[] */ 26 | private $errorFormatter; 27 | 28 | /** 29 | * @param null|callable(string, string): E[] $errorFormatter 30 | */ 31 | private function __construct(string $pattern, ?callable $errorFormatter = null) 32 | { 33 | $this->pattern = $pattern; 34 | 35 | /** @psalm-suppress PossiblyInvalidPropertyAssignmentValue */ 36 | $this->errorFormatter = is_callable($errorFormatter) ? 37 | $errorFormatter : 38 | /** 39 | * @return string[] 40 | */ 41 | function (string $pattern, string $data): array { 42 | return [self::MESSAGE]; 43 | }; 44 | } 45 | 46 | /** 47 | * @return self 48 | * @psalm-suppress MixedReturnTypeCoercion 49 | */ 50 | public static function withPattern(string $pattern): self 51 | { 52 | return new self($pattern); 53 | } 54 | 55 | /** 56 | * @template F 57 | * @param callable(string, string): F[] $errorFormatter 58 | * @return self 59 | */ 60 | public static function withPatternAndFormatter(string $pattern, callable $errorFormatter): self 61 | { 62 | return new self($pattern, $errorFormatter); 63 | } 64 | 65 | /** 66 | * @return self 67 | * @psalm-suppress MixedReturnTypeCoercion 68 | */ 69 | public static function withPatternAndTranslator(string $pattern, Translator $translator): self 70 | { 71 | return new self( 72 | $pattern, 73 | /** 74 | * @return string[] 75 | */ 76 | function (string $pattern, string $data) use ($translator): array { 77 | return [$translator->translate(self::MESSAGE)]; 78 | } 79 | ); 80 | } 81 | 82 | /** 83 | * @param string $data 84 | * @return ValidationResult 85 | */ 86 | public function validate($data, array $context = []): ValidationResult 87 | { 88 | $match = preg_match($this->pattern, $data); 89 | 90 | if (false === $match || 0 === $match) { 91 | /** @var ValidationResult $ret */ 92 | $ret = ValidationResult::errors(($this->errorFormatter)($this->pattern, $data)); 93 | 94 | return $ret; 95 | } 96 | 97 | return ValidationResult::valid($data); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Basic/Valid.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class Valid implements Validation 16 | { 17 | /** 18 | * @param A $data 19 | * @return ValidationResult 20 | */ 21 | public function validate($data, array $context = []): ValidationResult 22 | { 23 | return ValidationResult::valid($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Combinator/All.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class All implements Validation 21 | { 22 | /** @var Validation[] */ 23 | private $validations; 24 | 25 | /** 26 | * @var callable(E[], E[]): E[] 27 | */ 28 | private $errorFormatter; 29 | 30 | /** 31 | * @param Validation[] $validations 32 | * @param null|callable(E[], E[]): E[] $errorFormatter 33 | * @throws InvalidArgumentException 34 | */ 35 | private function __construct(array $validations, ?callable $errorFormatter = null) 36 | { 37 | Assert::allIsInstanceOf($validations, Validation::class); 38 | 39 | $this->validations = $validations; 40 | $this->errorFormatter = is_callable($errorFormatter) ? 41 | $errorFormatter : 42 | 'array_merge'; 43 | } 44 | 45 | /** 46 | * @template C 47 | * @template F 48 | * @template D 49 | * @param Validation[] $validations 50 | * @return self 51 | * @throws InvalidArgumentException 52 | */ 53 | public static function validations(array $validations): self 54 | { 55 | return new self($validations); 56 | } 57 | 58 | /** 59 | * @template C 60 | * @template F 61 | * @template D 62 | * @param Validation[] $validations 63 | * @param callable(F[], F[]): F[] $errorFormatter 64 | * @return self 65 | * @throws InvalidArgumentException 66 | */ 67 | public static function validationsWithFormatter(array $validations, callable $errorFormatter) 68 | { 69 | return new self($validations, $errorFormatter); 70 | } 71 | 72 | /** 73 | * @param A $data 74 | * @param array $context 75 | * @return ValidationResult 76 | */ 77 | public function validate($data, array $context = []): ValidationResult 78 | { 79 | /** @var ValidationResult $result */ 80 | $result = ValidationResult::valid($data); 81 | 82 | foreach ($this->validations as $validation) { 83 | $result = $result->join( 84 | $validation->validate($data, $context), 85 | /** 86 | * @param B $a 87 | * @param B $b 88 | * @return B 89 | */ 90 | function ($a, $b) { 91 | return $a; 92 | }, 93 | $this->errorFormatter 94 | ); 95 | } 96 | 97 | return $result; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Combinator/Any.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Any implements Validation 21 | { 22 | public const NOT_EVEN_ONE = 'any.not-even-one'; 23 | 24 | /** @var Validation[] */ 25 | private $validations; 26 | 27 | /** @var callable(E[]): E[][] */ 28 | private $errorFormatter; 29 | 30 | /** 31 | * @param Validation[] $validations 32 | * @param null|callable(E[]): E[][] $errorFormatter 33 | * @throws InvalidArgumentException 34 | */ 35 | private function __construct(array $validations, ?callable $errorFormatter = null) 36 | { 37 | Assert::allIsInstanceOf($validations, Validation::class); 38 | 39 | $this->validations = $validations; 40 | $this->errorFormatter = is_callable($errorFormatter) ? 41 | $errorFormatter : 42 | /** 43 | * @param E[] $messages 44 | * @return E[][] 45 | */ 46 | function (array $messages): array { 47 | return [ 48 | self::NOT_EVEN_ONE => $messages 49 | ]; 50 | }; 51 | } 52 | 53 | /** 54 | * @template C 55 | * @template F 56 | * @param Validation[] $validations 57 | * @return self 58 | * @throws InvalidArgumentException 59 | */ 60 | public static function validations(array $validations): self 61 | { 62 | return new self($validations); 63 | } 64 | 65 | /** 66 | * @template C 67 | * @template F 68 | * @param Validation[] $validations 69 | * @param callable(F[]): F[][] $errorFormatter 70 | * @return self 71 | * @throws InvalidArgumentException 72 | */ 73 | public static function validationsWithFormatter(array $validations, callable $errorFormatter): self 74 | { 75 | return new self($validations, $errorFormatter); 76 | } 77 | 78 | /** 79 | * @template C 80 | * @template F 81 | * @param Validation[] $validations 82 | * @param Translator $translator 83 | * @return self 84 | * @throws InvalidArgumentException 85 | */ 86 | public static function validationsWithTranslator(array $validations, Translator $translator): self 87 | { 88 | return new self( 89 | $validations, 90 | /** 91 | * @param F[] $messages 92 | * @return F[][] 93 | */ 94 | function (array $messages) use ($translator): array { 95 | return [ 96 | $translator->translate(self::NOT_EVEN_ONE) => $messages 97 | ]; 98 | } 99 | ); 100 | } 101 | 102 | /** 103 | * @param A $data 104 | * @param array $context 105 | * @return ValidationResult 106 | */ 107 | public function validate($data, array $context = []): ValidationResult 108 | { 109 | /** @var ValidationResult $result */ 110 | $result = ValidationResult::errors([]); 111 | 112 | foreach ($this->validations as $validation) { 113 | $result = $result->meet( 114 | $validation->validate($data, $context), 115 | /** 116 | * @param A $x 117 | * @param A $y 118 | * @return A 119 | */ 120 | function ($x, $y) { 121 | return $x; 122 | }, 123 | /** 124 | * @param A $x 125 | * @return A 126 | */ 127 | function ($x) { 128 | return $x; 129 | }, 130 | /** 131 | * @param A $x 132 | * @return A 133 | */ 134 | function ($x) { 135 | return $x; 136 | }, 137 | 'array_merge' 138 | ); 139 | } 140 | 141 | return $result 142 | ->mapErrors($this->errorFormatter) 143 | ->map( 144 | function () use ($data) { 145 | return $data; 146 | } 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Combinator/AnyElement.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class AnyElement implements Validation 18 | { 19 | /** @var Validation */ 20 | private $elementValidation; 21 | 22 | /** @var callable(array-key, E[][], E[]): E[][] */ 23 | private $errorFormatter; 24 | 25 | /** 26 | * @param Validation $validation 27 | * @param null|callable(array-key, E[][], E[]): E[][] $errorFormatter 28 | */ 29 | private function __construct(Validation $validation, ?callable $errorFormatter = null) 30 | { 31 | $this->elementValidation = $validation; 32 | $this->errorFormatter = is_callable($errorFormatter) ? 33 | $errorFormatter : 34 | /** 35 | * @param array-key $key 36 | * @param E[][] $resultMessages 37 | * @param E[] $validationMessages 38 | * @return E[][] 39 | */ 40 | function ($key, array $resultMessages, array $validationMessages): array { 41 | $resultMessages[$key] = $validationMessages; 42 | 43 | return $resultMessages; 44 | }; 45 | } 46 | 47 | /** 48 | * @template C 49 | * @template F 50 | * @param Validation $validation 51 | * @return self 52 | */ 53 | public static function validation(Validation $validation): self 54 | { 55 | return new self($validation); 56 | } 57 | 58 | /** 59 | * @template C 60 | * @template F 61 | * @param Validation $validation 62 | * @param callable(array-key, F[][], F[]): F[][] $errorFormatter 63 | * @return self 64 | */ 65 | public static function validationWithFormatter(Validation $validation, callable $errorFormatter): self 66 | { 67 | return new self($validation, $errorFormatter); 68 | } 69 | 70 | /** 71 | * @param A[] $data 72 | * @param array $context 73 | * @return ValidationResult 74 | */ 75 | public function validate($data, array $context = []): ValidationResult 76 | { 77 | $errorFormatter = $this->errorFormatter; 78 | 79 | /** @var ValidationResult $result */ 80 | $result = ValidationResult::errors([]); 81 | 82 | foreach ($data as $key => $element) { 83 | $result = $result->meet( 84 | $this->elementValidation->validate($element, $context), 85 | /** 86 | * @param A[] $x 87 | * @param A $y 88 | * @return A[] 89 | */ 90 | function (array $x, $y) { 91 | return $x; 92 | }, 93 | /** 94 | * @param A[] $x 95 | * @return A[] 96 | */ 97 | function (array $x) { 98 | return $x; 99 | }, 100 | /** 101 | * @param A $x 102 | * @return A[] 103 | */ 104 | function ($x) { 105 | return [$x]; 106 | }, 107 | /** 108 | * @param E[][] $resultMessages 109 | * @param E[] $validationMessages 110 | * @return E[][] 111 | */ 112 | function (array $resultMessages, array $validationMessages) use ($key, $errorFormatter) { 113 | return $errorFormatter($key, $resultMessages, $validationMessages); 114 | } 115 | ); 116 | } 117 | 118 | return $result->map( 119 | function () use ($data) { 120 | return $data; 121 | } 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Combinator/Apply.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class Apply implements Validation 18 | { 19 | /** @var Validation */ 20 | private $validation; 21 | 22 | /** 23 | * @var ValidationResult 24 | */ 25 | private $validationFunction; 26 | 27 | /** 28 | * @param Validation $validation 29 | * @param ValidationResult $validationFunction 30 | */ 31 | private function __construct(Validation $validation, ValidationResult $validationFunction) 32 | { 33 | $this->validation = $validation; 34 | $this->validationFunction = $validationFunction; 35 | } 36 | 37 | /** 38 | * @template D 39 | * @template H 40 | * @template F 41 | * @template G 42 | * @param Validation $validation 43 | * @param ValidationResult $validationFunction 44 | * @return self 45 | */ 46 | public static function to(Validation $validation, ValidationResult $validationFunction): self 47 | { 48 | return new self($validation, $validationFunction); 49 | } 50 | 51 | /** 52 | * @param A $data 53 | * @param array $context 54 | * @return ValidationResult 55 | */ 56 | public function validate($data, array $context = []): ValidationResult 57 | { 58 | return $this->validation->validate($data, $context)->apply($this->validationFunction); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Combinator/Associative.php: -------------------------------------------------------------------------------- 1 | validations = $validations; 28 | } 29 | 30 | /** 31 | * @param Validation[] $validations 32 | * @return self 33 | */ 34 | public static function validations(array $validations) 35 | { 36 | return new self($validations); 37 | } 38 | 39 | public function validate($data, array $context = []): ValidationResult 40 | { 41 | $wholeValidation = Sequence::validations([ 42 | new IsArray(), 43 | All::validations(array_map( 44 | /** 45 | * @param array-key $key 46 | * @param Validation $validation 47 | * @return Validation 48 | */ 49 | static function ($key, Validation $validation) { 50 | return MapErrors::to( 51 | Sequence::validations([ 52 | HasKey::withKey($key), 53 | Focus::on( 54 | /** 55 | * @param array $wholeData 56 | * @return mixed 57 | */ 58 | static function (array $wholeData) use ($key) { 59 | return $wholeData[$key]; 60 | }, 61 | $validation 62 | ) 63 | ]), 64 | /** 65 | * @param array $messages 66 | * @return array 67 | */ 68 | static function (array $messages) use ($key): array { 69 | return [$key => $messages]; 70 | } 71 | ); 72 | }, 73 | array_keys($this->validations), 74 | $this->validations 75 | )) 76 | ]); 77 | 78 | return $wholeValidation->validate($data, $context); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Combinator/Bind.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class Bind implements Validation 18 | { 19 | /** @var Validation */ 20 | private $validation; 21 | 22 | /** 23 | * @var callable(B): ValidationResult 24 | */ 25 | private $function; 26 | 27 | /** 28 | * @param Validation $validation 29 | * @param callable(B): ValidationResult $function 30 | */ 31 | private function __construct(Validation $validation, callable $function) 32 | { 33 | $this->validation = $validation; 34 | $this->function = $function; 35 | } 36 | 37 | /** 38 | * @template D 39 | * @template H 40 | * @template F 41 | * @template G 42 | * @param Validation $validation 43 | * @param callable(F): ValidationResult $function 44 | * @return self 45 | */ 46 | public static function to(Validation $validation, callable $function): self 47 | { 48 | return new self($validation, $function); 49 | } 50 | 51 | /** 52 | * @param A $data 53 | * @param array $context 54 | * @return ValidationResult 55 | */ 56 | public function validate($data, array $context = []): ValidationResult 57 | { 58 | return $this->validation->validate($data, $context)->bind($this->function); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Combinator/EveryElement.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class EveryElement implements Validation 19 | { 20 | /** @var Validation */ 21 | private $elementValidation; 22 | 23 | /** @var callable callable(array-key, E[][], E[]): E[][] */ 24 | private $errorFormatter; 25 | 26 | /** 27 | * @param Validation $validation 28 | * @param null|callable(array-key, E[][], E[]): E[][] $errorFormatter 29 | */ 30 | private function __construct(Validation $validation, ?callable $errorFormatter = null) 31 | { 32 | $this->elementValidation = $validation; 33 | $this->errorFormatter = is_callable($errorFormatter) ? 34 | $errorFormatter : 35 | /** 36 | * @param array-key $key 37 | * @param E[][] $resultMessages 38 | * @param E[] $validationMessages 39 | * @return E[][] 40 | */ 41 | function ($key, array $resultMessages, array $validationMessages): array { 42 | $resultMessages[$key] = $validationMessages; 43 | 44 | return $resultMessages; 45 | }; 46 | } 47 | 48 | /** 49 | * @template C 50 | * @template F 51 | * @template D 52 | * @param Validation $validation 53 | * @return self 54 | */ 55 | public static function validation(Validation $validation): self 56 | { 57 | return new self($validation); 58 | } 59 | 60 | /** 61 | * @template C 62 | * @template F 63 | * @template D 64 | * @param Validation $validation 65 | * @param callable(array-key, F[][], F[]): F[][] $errorFormatter 66 | * @return self 67 | */ 68 | public static function validationWithFormatter(Validation $validation, callable $errorFormatter): self 69 | { 70 | return new self($validation, $errorFormatter); 71 | } 72 | 73 | /** 74 | * @param A[] $data 75 | * @param array $context 76 | * @return ValidationResult 77 | */ 78 | public function validate($data, array $context = []): ValidationResult 79 | { 80 | /** @var callable(array-key, E[][], E[]): E[][] $errorFormatter */ 81 | $errorFormatter = $this->errorFormatter; 82 | 83 | /** @var ValidationResult $result */ 84 | $result = ValidationResult::valid($data); 85 | 86 | foreach ($data as $key => $element) { 87 | $result = $result->join( 88 | $this->elementValidation->validate($element, $context), 89 | /** 90 | * @psalm-param B[] $result 91 | * @psalm-param B $next 92 | * @return B[] 93 | */ 94 | function (array $result, $next) { 95 | return $result; 96 | }, 97 | /** 98 | * @param E[][] $resultMessages 99 | * @param E[] $validationMessages 100 | * @return E[][] 101 | */ 102 | function (array $resultMessages, array $validationMessages) use ($key, $errorFormatter): array { 103 | return $errorFormatter($key, $resultMessages, $validationMessages); 104 | } 105 | ); 106 | } 107 | 108 | return $result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Combinator/Focus.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Focus implements Validation 17 | { 18 | /** @var callable(A): B */ 19 | private $focus; 20 | 21 | /** @var Validation */ 22 | private $validation; 23 | 24 | /** 25 | * @param callable(A): B $focus 26 | * @param Validation $validation 27 | */ 28 | private function __construct(callable $focus, Validation $validation) 29 | { 30 | $this->focus = $focus; 31 | $this->validation = $validation; 32 | } 33 | 34 | /** 35 | * @template C 36 | * @template D 37 | * @template F 38 | * @param callable(C): D $focus 39 | * @param Validation $validation 40 | * @return self 41 | */ 42 | public static function on(callable $focus, Validation $validation): self 43 | { 44 | return new self($focus, $validation); 45 | } 46 | 47 | /** 48 | * @param A $data 49 | * @param array $context 50 | * @return ValidationResult 51 | */ 52 | public function validate($data, array $context = []): ValidationResult 53 | { 54 | return $this->validation->validate(($this->focus)($data), $context) 55 | // would really need a Lens here to update the outer value applying the callable to the inner value 56 | ->map( 57 | /** 58 | * @return A 59 | */ 60 | function () use ($data) { 61 | return $data; 62 | } 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Combinator/Map.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class Map implements Validation 18 | { 19 | /** @var Validation */ 20 | private $validation; 21 | 22 | /** @var callable(B): C */ 23 | private $function; 24 | 25 | /** 26 | * @param Validation $validation 27 | * @param callable(B): C $function 28 | */ 29 | private function __construct( 30 | Validation $validation, 31 | callable $function 32 | ) { 33 | $this->validation = $validation; 34 | $this->function = $function; 35 | } 36 | 37 | /** 38 | * @template D 39 | * @template H 40 | * @template F 41 | * @template G 42 | * @param Validation $validation 43 | * @param callable(F): G $function 44 | * @return self 45 | */ 46 | public static function to( 47 | Validation $validation, 48 | callable $function 49 | ): self { 50 | return new self($validation, $function); 51 | } 52 | 53 | /** 54 | * @param A $data 55 | * @param array $context 56 | * @return ValidationResult 57 | */ 58 | public function validate($data, array $context = []): ValidationResult 59 | { 60 | return $this->validation->validate($data, $context)->map($this->function); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Combinator/MapErrors.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class MapErrors implements Validation 18 | { 19 | /** @var Validation */ 20 | private $validation; 21 | 22 | /** 23 | * @var callable(E[]): F[] 24 | */ 25 | private $function; 26 | 27 | /** 28 | * @param Validation $validation 29 | * @param callable(E[]): F[] $function 30 | */ 31 | private function __construct( 32 | Validation $validation, 33 | callable $function 34 | ) { 35 | $this->validation = $validation; 36 | $this->function = $function; 37 | } 38 | 39 | /** 40 | * @template C 41 | * @template G 42 | * @template H 43 | * @template D 44 | * @param Validation $validation 45 | * @param callable(G[]): H[] $function 46 | * @return self 47 | */ 48 | public static function to( 49 | Validation $validation, 50 | callable $function 51 | ): self { 52 | return new self($validation, $function); 53 | } 54 | 55 | /** 56 | * @param A $data 57 | * @param array $context 58 | * @return ValidationResult 59 | */ 60 | public function validate($data, array $context = []): ValidationResult 61 | { 62 | return $this->validation->validate($data, $context)->mapErrors($this->function); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Combinator/Sequence.php: -------------------------------------------------------------------------------- 1 | validations = $validations; 29 | } 30 | 31 | /** 32 | * @param Validation[] $validations 33 | * @return self 34 | * @throws InvalidArgumentException 35 | */ 36 | public static function validations(array $validations): self 37 | { 38 | return new self($validations); 39 | } 40 | 41 | public function validate($data, array $context = []): ValidationResult 42 | { 43 | return array_reduce( 44 | $this->validations, 45 | function (ValidationResult $carry, Validation $validation) use ($context): ValidationResult { 46 | return $carry->process( 47 | /** @psalm-suppress MissingClosureParamType */ 48 | function ($validData) use ($validation, $context): ValidationResult { 49 | return $validation->validate($validData, $context); 50 | }, 51 | function () use ($carry): ValidationResult { 52 | return $carry; 53 | } 54 | ); 55 | }, 56 | ValidationResult::valid($data) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Combinator/TranslateErrors.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class TranslateErrors implements Validation 22 | { 23 | /** @var Validation */ 24 | private $validation; 25 | 26 | /** @var Translator */ 27 | private $translator; 28 | 29 | /** 30 | * @param Validation $validation 31 | */ 32 | public function __construct(Validation $validation, Translator $translator) 33 | { 34 | $this->validation = $validation; 35 | $this->translator = $translator; 36 | } 37 | 38 | /** 39 | * @template C 40 | * @template F 41 | * @template D 42 | * @param Validation $validation 43 | * @return self 44 | */ 45 | public static function validationWithTranslator(Validation $validation, Translator $translator): self 46 | { 47 | return new self($validation, $translator); 48 | } 49 | 50 | /** 51 | * @param A $data 52 | * @param array $context 53 | * @return ValidationResult 54 | */ 55 | public function validate($data, array $context = []): ValidationResult 56 | { 57 | return MapErrors::to($this->validation, Closure::fromCallable([$this, 'translateNestedErrors']))->validate($data, $context); 58 | } 59 | 60 | /** 61 | * @param E[] $messages 62 | * @return E[] 63 | * @psalm-suppress InvalidReturnType 64 | */ 65 | private function translateNestedErrors(array $messages): array 66 | { 67 | $translator = $this->translator; 68 | 69 | /** @psalm-suppress InvalidReturnStatement */ 70 | return array_map( 71 | function ($message) use ($translator) { 72 | if (is_string($message)) { 73 | return $translator->translate($message); 74 | } 75 | 76 | if (is_array($message)) { 77 | return $this->translateNestedErrors($message); 78 | } 79 | 80 | return $message; 81 | }, 82 | $messages 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Equality.php: -------------------------------------------------------------------------------- 1 | isValid = $isValid; 34 | $this->validContent = $validContent; 35 | $this->messages = $messages; 36 | } 37 | 38 | /** 39 | * @template B 40 | * @template F 41 | * @param B $validContent 42 | * @return self 43 | */ 44 | public static function valid($validContent): self 45 | { 46 | return new self(true, $validContent, []); 47 | } 48 | 49 | /** 50 | * @template B 51 | * @template F 52 | * @param F[] $messages 53 | * @return self 54 | */ 55 | public static function errors(array $messages): self 56 | { 57 | return new self(false, null, $messages); 58 | } 59 | 60 | /** 61 | * @template F 62 | * @template B 63 | * @template G 64 | * @template C 65 | * @param self $that 66 | * @param callable(A, B): C $joinValid 67 | * @param callable(E[], F[]): G[] $joinErrors 68 | * @return self 69 | */ 70 | public function join(self $that, callable $joinValid, callable $joinErrors): self 71 | { 72 | if (! $this->isValid || ! $that->isValid) { 73 | return self::errors($joinErrors($this->messages, $that->messages)); 74 | } 75 | 76 | /** @var A $thisContent */ 77 | $thisContent = $this->validContent; 78 | /** @var B $thatContent */ 79 | $thatContent = $that->validContent; 80 | 81 | return self::valid($joinValid($thisContent, $thatContent)); 82 | } 83 | 84 | /** 85 | * @template F 86 | * @template B 87 | * @template G 88 | * @template C 89 | * @param self $that 90 | * @param callable(A, B): C $joinBothValid 91 | * @param callable(A): C $joinThisValid 92 | * @param callable(B): C $joinThatValid 93 | * @param callable(E[], F[]): G[] $joinErrors 94 | * @return self 95 | */ 96 | public function meet( 97 | self $that, 98 | callable $joinBothValid, 99 | callable $joinThisValid, 100 | callable $joinThatValid, 101 | callable $joinErrors 102 | ): self { 103 | $thisContent = $this->validContent; 104 | $thatContent = $that->validContent; 105 | 106 | if ($this->isValid && $that->isValid) { 107 | /** 108 | * @var A $thisContent 109 | * @var B $thatContent 110 | */ 111 | 112 | return self::valid($joinBothValid($thisContent, $thatContent)); 113 | } 114 | 115 | if ($this->isValid) { 116 | /** @var A $thisContent */ 117 | 118 | return self::valid($joinThisValid($thisContent)); 119 | } 120 | 121 | if ($that->isValid) { 122 | /** @var B $thatContent */ 123 | 124 | return self::valid($joinThatValid($thatContent)); 125 | } 126 | 127 | return self::errors($joinErrors($this->messages, $that->messages)); 128 | } 129 | 130 | /** 131 | * @template B 132 | * @param callable(A): B $processValid 133 | * @param callable(E[]): B $processErrors 134 | * @return B 135 | */ 136 | public function process( 137 | callable $processValid, 138 | callable $processErrors 139 | ) { 140 | if (! $this->isValid) { 141 | return $processErrors($this->messages); 142 | } 143 | 144 | /** @psalm-suppress PossiblyNullArgument */ 145 | return $processValid($this->validContent); 146 | } 147 | 148 | /** 149 | * @template B 150 | * @param callable(A): B $map 151 | * @return self 152 | */ 153 | public function map(callable $map): self 154 | { 155 | return $this->process( 156 | /** @param A $validContent */ 157 | function ($validContent) use ($map): self { 158 | return self::valid($map($validContent)); 159 | }, 160 | function (array $messages): self { 161 | return self::errors($messages); 162 | } 163 | ); 164 | } 165 | 166 | /** 167 | * @template F 168 | * @param callable(E[]): F[] $map 169 | * @return self 170 | */ 171 | public function mapErrors(callable $map): self 172 | { 173 | return $this->process( 174 | /** @param mixed $validContent */ 175 | function ($validContent): self { 176 | return self::valid($validContent); 177 | }, 178 | function (array $messages) use ($map): self { 179 | return self::errors($map($messages)); 180 | } 181 | ); 182 | } 183 | 184 | /** 185 | * @template B 186 | * @param ValidationResult $apply 187 | * @return self 188 | */ 189 | public function apply(ValidationResult $apply): self 190 | { 191 | /** @psalm-suppress MixedArgumentTypeCoercion */ 192 | return $apply->process( 193 | /** 194 | * @param callable(A): B $validApply 195 | * @return self 196 | */ 197 | function (callable $validApply): self { 198 | return $this->map($validApply); 199 | }, 200 | /** @return self */ 201 | function (array $applyMessages) { 202 | return $this->process( 203 | /** @param A $validContent */ 204 | function ($validContent) use ($applyMessages): self { 205 | return self::errors($applyMessages); 206 | }, 207 | function (array $messages) use ($applyMessages): self { 208 | return self::errors(array_merge($applyMessages, $messages)); 209 | } 210 | ); 211 | } 212 | ); 213 | } 214 | 215 | /** 216 | * @template B 217 | * @param callable(A): self $bind 218 | * @return self 219 | */ 220 | public function bind(callable $bind): self 221 | { 222 | return $this->process( 223 | /** @param A $validContent */ 224 | function ($validContent) use ($bind): self { 225 | return $bind($validContent); 226 | }, 227 | function (array $messages): self { 228 | return self::errors($messages); 229 | } 230 | ); 231 | } 232 | 233 | /** 234 | * @param self $that 235 | * @return bool 236 | */ 237 | public function equals($that): bool 238 | { 239 | if ( 240 | is_object($this->validContent) && is_object($that->validContent) && 241 | get_class($this->validContent) === get_class($that->validContent) && 242 | $this->validContent instanceof Equality 243 | ) { 244 | $contentEquality = $this->validContent->equals($that->validContent); 245 | } else { 246 | $contentEquality = $this->validContent === $that->validContent; 247 | } 248 | 249 | return ($this->isValid && $that->isValid && $contentEquality) || 250 | (!$this->isValid && !$that->isValid && $this->messages === $that->messages); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Result/functions.php: -------------------------------------------------------------------------------- 1 | T 15 | * @return Closure with signature (ValidationResult T1, ValidationResult T2, ...) -> ValidationResult T 16 | * 17 | * @psalm-return Closure(): ValidationResult 18 | */ 19 | function lift(callable $f): Closure 20 | { 21 | $innerLift = 22 | /** 23 | * @return Closure 24 | * 25 | * @psalm-return Closure(): ValidationResult 26 | */ 27 | static function ( 28 | callable $f, 29 | ?int $numberOfParameters = null, 30 | array $parameters = [] 31 | ) use (&$innerLift): Closure { 32 | if (null === $numberOfParameters) { 33 | // retrieve number of parameters from reflection 34 | $fClosure = Closure::fromCallable($f); 35 | $fRef = new ReflectionFunction($fClosure); 36 | $numberOfParameters = $fRef->getNumberOfParameters(); 37 | } 38 | 39 | /** @return ValidationResult|Closure */ 40 | /** @psalm-suppress MissingClosureReturnType */ 41 | return static function () use ($f, $numberOfParameters, $parameters, $innerLift) { 42 | // collect all necessary parameters 43 | 44 | /** @var array $newParameters */ 45 | $newParameters = array_merge($parameters, func_get_args()); 46 | 47 | if (count($newParameters) >= $numberOfParameters) { 48 | if ([] === $newParameters) { 49 | return ValidationResult::valid($f()); 50 | } 51 | 52 | $firstParameter = $newParameters[0]; 53 | 54 | $result = $firstParameter->map(curry($f)); 55 | 56 | foreach (array_slice($newParameters, 1) as $validatedParameter) { 57 | $result = $validatedParameter->apply($result); 58 | } 59 | 60 | return $result; 61 | } 62 | 63 | return $innerLift($f, $numberOfParameters, $newParameters); 64 | }; 65 | }; 66 | 67 | return $innerLift($f); 68 | } 69 | 70 | /** 71 | * @param callable[] $fs every function takes as arguments the unwrapped results of the previous one and returns a 72 | * ValidationResult 73 | * @return ValidationResult 74 | */ 75 | function sdo(callable ...$fs): ValidationResult 76 | { 77 | Assert::notEmpty($fs, 'do_ must receive at least one callable'); 78 | 79 | $result = ValidationResult::valid(null); 80 | 81 | foreach ($fs as $f) { 82 | $result = $result->bind($f); 83 | } 84 | 85 | return $result; 86 | } 87 | 88 | final class DoPartialTempResult 89 | { 90 | /** 91 | * @var callable 92 | */ 93 | private $f; 94 | 95 | /** 96 | * @var array 97 | */ 98 | private $arguments; 99 | 100 | private function __construct(callable $f, array $arguments) 101 | { 102 | $this->f = $f; 103 | $this->arguments = $arguments; 104 | } 105 | 106 | public static function fromCallableAndArguments(callable $f, array $arguments): ValidationResult 107 | { 108 | return ValidationResult::valid(new self($f, $arguments)); 109 | } 110 | 111 | public static function fromPreviousAndCallable(ValidationResult $previous, callable $f): ValidationResult 112 | { 113 | return $previous->bind(function (DoPartialTempResult $doPartialTempResult) use ($f): ValidationResult { 114 | $lastArgumentResult = $doPartialTempResult(); 115 | 116 | /** @psalm-suppress MissingClosureParamType */ 117 | return $lastArgumentResult->bind(function ($lastArgument) use ($doPartialTempResult, $f): ValidationResult { 118 | $fArguments = array_merge($doPartialTempResult->arguments, [$lastArgument]); 119 | 120 | return self::fromCallableAndArguments($f, $fArguments); 121 | }); 122 | }); 123 | } 124 | 125 | public function __invoke(): ValidationResult 126 | { 127 | return call_user_func_array($this->f, $this->arguments); 128 | } 129 | } 130 | 131 | /** 132 | * @param callable[] $fs every function takes as arguments the unwrapped results of the previous one and returns a 133 | * ValidationResult 134 | * @return ValidationResult 135 | */ 136 | function mdo(callable ...$fs): ValidationResult 137 | { 138 | Assert::notEmpty($fs, 'do__ must receive at least one callable'); 139 | 140 | $doPartialTempResult = DoPartialTempResult::fromCallableAndArguments($fs[0], []); 141 | 142 | foreach (array_slice($fs, 1) as $f) { 143 | $doPartialTempResult = DoPartialTempResult::fromPreviousAndCallable($doPartialTempResult, $f); 144 | } 145 | 146 | return $doPartialTempResult->bind(function (DoPartialTempResult $doPartialTempResult): ValidationResult { 147 | return $doPartialTempResult(); 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /src/Translator/Combinator/Coalesce.php: -------------------------------------------------------------------------------- 1 | translators = $translators; 22 | } 23 | 24 | public static function withTranslators(?Translator ...$translators): self 25 | { 26 | return new self($translators); 27 | } 28 | 29 | public function translate(string $string): string 30 | { 31 | foreach ($this->translators as $translator) { 32 | if ($translator instanceof Translator) { 33 | return $translator->translate($string); 34 | } 35 | } 36 | 37 | return $string; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Translator/ConstantTranslator.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 17 | } 18 | 19 | public static function withTranslation(string $translation): self 20 | { 21 | return new self($translation); 22 | } 23 | 24 | public function translate(string $string): string 25 | { 26 | return $this->translation; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Translator/IdentityTranslator.php: -------------------------------------------------------------------------------- 1 | key value dictionary of translations 13 | */ 14 | private $dictionary; 15 | 16 | /** 17 | * @param array $dictionary 18 | */ 19 | private function __construct(array $dictionary) 20 | { 21 | $this->dictionary = $dictionary; 22 | } 23 | 24 | /** 25 | * @param array $dictionary 26 | */ 27 | public static function withDictionary(array $dictionary): self 28 | { 29 | return new self($dictionary); 30 | } 31 | 32 | public function translate(string $string): string 33 | { 34 | return array_key_exists($string, $this->dictionary) ? 35 | $this->dictionary[$string] : 36 | $string; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Translator/Translator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function validate($data, array $context = []): ValidationResult; 22 | } 23 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | $something 14 | * @return Closure with signature $a1 -> ($a2 -> (... -> $something)) 15 | * 16 | * @psalm-return Closure(): callable 17 | */ 18 | function curry(callable $f): Closure 19 | { 20 | $innerCurry = 21 | /** 22 | * @return Closure 23 | * 24 | * @psalm-return Closure(): callable 25 | */ 26 | static function ( 27 | callable $f, 28 | ?int $numberOfParameters = null, 29 | array $parameters = [] 30 | ) use (&$innerCurry): Closure { 31 | if (null === $numberOfParameters) { 32 | // retrieve number of parameters from reflection 33 | $fClosure = Closure::fromCallable($f); 34 | $fRef = new ReflectionFunction($fClosure); 35 | $numberOfParameters = $fRef->getNumberOfParameters(); 36 | } 37 | 38 | /** @psalm-suppress MissingClosureReturnType */ 39 | return static function () use ($f, $numberOfParameters, $parameters, $innerCurry) { 40 | /** @var array $newParameters */ 41 | $newParameters = array_merge($parameters, func_get_args()); 42 | 43 | if (count($newParameters) >= $numberOfParameters) { 44 | return call_user_func_array($f, $newParameters); 45 | } 46 | 47 | return $innerCurry($f, $numberOfParameters, $newParameters); 48 | }; 49 | }; 50 | 51 | return $innerCurry($f); 52 | } 53 | 54 | /** 55 | * @param callable $f with signature $a1 -> ($a2 -> (... -> $something)) 56 | * @return Closure with signature ($a1, $a2, ...) -> $something 57 | * 58 | * @psalm-return Closure(... array): mixed 59 | */ 60 | function uncurry(callable $f): Closure 61 | { 62 | /** 63 | * @psalm-suppress MissingClosureParamType 64 | * @psalm-suppress MissingClosureReturnType 65 | */ 66 | return static function (...$params) use ($f) { 67 | if ([] === $params) { 68 | return $f(); 69 | } 70 | 71 | $firstParam = $params[0]; 72 | 73 | $firstApplication = $f($firstParam); 74 | 75 | if (! is_callable($firstApplication)) { 76 | return $firstApplication; 77 | } 78 | 79 | return uncurry($firstApplication)(...array_slice($params, 1)); 80 | }; 81 | } 82 | --------------------------------------------------------------------------------