diff --git a/AGENTS.md b/AGENTS.md index 2427e44..56f6e12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ This file outlines the requirements and best practices for adding new assertion - **Version Compliance**: Always use the most current version of AGENTS.md and do not rely on cached or outdated copies. Refresh and re-read AGENTS.md before every change to ensure compliance with the latest requirements. - **Branching and Commits**: It is forbidden to commit directly to the `main` branch. All changes must be added via pull request from a feature branch. If the current branch is `main`, MUST checkout to a new branch before changing any files. Do not push changes automatically—only push after explicit user request. - **Pull Requests**: When creating a pull request, update the PR description with a detailed summary of changes, including new methods added, files modified, and any breaking changes. Ensure the description follows the format: Summary, Changes, Testing, Validation. +- **Dependency Management**: Remove unused dependencies from composer.json. If a tool (e.g., Psalm) is no longer used, remove its require-dev entry and update scripts accordingly. - **Method Signature**: All new methods must be public, accept an optional `$message` parameter (string, default empty), and return `self` to enable fluent chaining. - **Type Safety**: Specify strict types for parameters where applicable (e.g., `int|float` for numeric comparisons). Avoid `mixed` unless necessary. - **PHPUnit Integration**: Use appropriate PHPUnit assertion methods (e.g., `Assert::assertLessThan`) without named parameters for compatibility. @@ -49,7 +50,7 @@ This file outlines the requirements and best practices for adding new assertion ## Validation Steps - **Run Tests**: Execute `./vendor/bin/phpunit tests/FluentAssertions/Asserts/MethodName/MethodNameTest.php` to verify implementation. -- **Lint and Typecheck**: Run linting and type checking commands (e.g., via composer scripts or direct tools) to ensure code quality. +- **Lint and Typecheck**: Run PHPStan static analysis via `composer run analyze` to ensure code quality. - **Integration**: Ensure the method works in the overall fluent chain without breaking existing functionality. ## Example Workflow for Adding `isGreaterThan` diff --git a/README.md b/README.md index f579213..3936d13 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,16 @@ fact([])->isEmptyArray(); // Passes fact([1, 2])->isEmptyArray(); // Fails fact([1, 2])->isNotEmptyArray(); // Passes -fact([])->isNotEmptyArray(); // Fails +fact([])->isNotEmptyArray(); // Fails +fact([2, 4, 6])->every(fn($v) => $v % 2 === 0); // Passes +fact([1, 2, 3])->every(fn($v) => $v > 5); // Fails + +fact([1, 2, 3])->some(fn($v) => $v > 2); // Passes +fact([1, 2, 3])->some(fn($v) => $v > 10); // Fails + +fact([1, 2, 3])->none(fn($v) => $v > 10); // Passes +fact([1, 2, 3])->none(fn($v) => $v > 2); // Fails ``` ### Boolean assertions @@ -84,8 +92,7 @@ fact(true)->notFalse(); // Passes fact(false)->notFalse(); // Fails ``` - -### Comparison and Equality assertions +### Comparison and equality assertions ```php fact(42)->is(42); // Passes fact(42)->is('42'); // Fails due to type difference @@ -97,7 +104,6 @@ fact(42)->not(43); // Passes fact(42)->not(42); // Fails ``` - ### Null assertions ```php fact(null)->null(); // Passes @@ -114,7 +120,7 @@ fact(10)->isLowerThan(5); // Fails fact(10)->isGreaterThan(5); // Passes fact(5)->isGreaterThan(10); // Fails - + fact(5)->isPositive(); // Passes fact(-3)->isPositive(); // Fails @@ -129,12 +135,6 @@ fact(5)->isBetween(1, 10); // Passes fact(15)->isBetween(1, 10); // Fails ``` -### Special assertions -```php -fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID) -fact('invalid-ulid')->ulid(); // Fails -``` - ### String assertions ```php fact('abc123')->matchesRegularExpression('/^[a-z]+\d+$/'); // Passes @@ -175,6 +175,9 @@ fact('invalid json')->isJson(); // Fails fact('user@example.com')->isValidEmail(); // Passes fact('invalid-email')->isValidEmail(); // Fails + +fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID) +fact('invalid-ulid')->ulid(); // Fails ``` ### Type Checking assertions @@ -205,6 +208,18 @@ fact(1)->isBool(); // Fails fact([1, 2])->isArray(); // Passes fact('not array')->isArray(); // Fails + +fact(fopen('php://memory', 'r'))->isResource(); // Passes +fact('string')->isResource(); // Fails + +fact('strlen')->isCallable(); // Passes +fact(123)->isCallable(); // Fails + +fact(3.14)->isFloat(); // Passes +fact(42)->isFloat(); // Fails + +fact(true)->isBool(); // Passes +fact(1)->isBool(); // Fails ``` ## Pull requests are always welcome diff --git a/composer.json b/composer.json index 27380c5..257697e 100644 --- a/composer.json +++ b/composer.json @@ -34,15 +34,12 @@ }, "minimum-stability": "stable", "scripts": { - "analyze": [ - "@phpstan", - "@psalm" - ], - "phpstan": "./vendor/bin/phpstan analyse", - "psalm": "./vendor/bin/psalm.phar --config=psalm.xml" + "analyze": [ + "@phpstan" + ], + "phpstan": "./vendor/bin/phpstan analyse" }, - "require-dev": { - "psalm/phar": "^6.14", - "phpstan/phpstan": "^2.1" - } + "require-dev": { + "phpstan/phpstan": "^2.1" + } } diff --git a/phpunit.xml b/phpunit.xml index 9db7f9a..17f7cb5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,6 +4,21 @@ tests + + tests/FluentAssertions/Asserts + + + tests/FluentAssertions/Asserts + + + tests/FluentAssertions/Asserts + + + tests/FluentAssertions/Asserts + + + tests/FluentAssertions/Asserts + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 2edfffe..0000000 --- a/psalm.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/FluentAssertions.php b/src/FluentAssertions.php index 3ca73ea..23bfa3b 100644 --- a/src/FluentAssertions.php +++ b/src/FluentAssertions.php @@ -9,7 +9,6 @@ use K2gl\PHPUnitFluentAssertions\Traits\ComparisonAndEqualityAssertions; use K2gl\PHPUnitFluentAssertions\Traits\NullAssertions; use K2gl\PHPUnitFluentAssertions\Traits\NumericAssertions; -use K2gl\PHPUnitFluentAssertions\Traits\SpecialAssertions; use K2gl\PHPUnitFluentAssertions\Traits\StringAssertions; use K2gl\PHPUnitFluentAssertions\Traits\TypeCheckingAssertions; @@ -22,7 +21,6 @@ class FluentAssertions use StringAssertions; use ArrayAssertions; use TypeCheckingAssertions; - use SpecialAssertions; public function __construct( public readonly mixed $variable = null diff --git a/src/Traits/ArrayAssertions.php b/src/Traits/ArrayAssertions.php index 973cf2c..2407a1d 100644 --- a/src/Traits/ArrayAssertions.php +++ b/src/Traits/ArrayAssertions.php @@ -240,4 +240,118 @@ public function isNotEmptyArray(string $message = ''): self return $this; } + + /** + * Asserts that every element in the array satisfies the given callback. + * + * This method checks if all elements pass the condition defined by the callback. + * The callback receives the value and optionally the key. + * + * Example usage: + * fact([2, 4, 6])->every(fn($v) => $v % 2 === 0); // Passes + * fact([1, 2, 3])->every(fn($v) => $v > 5); // Fails + * + * @param callable $callback The function to test each element (receives value and key). + * @param string $message Optional custom error message. + * + * @return self Enables fluent chaining of assertion methods. + */ + public function every(callable $callback, string $message = ''): self + { + $array = $this->variable; + + if (!is_array($array)) { + Assert::assertTrue(false, $message ?: 'Variable is not an array.'); + } + + if (empty($array)) { + Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.'); + } + + foreach ($array as $key => $value) { + if (!$callback($value, $key)) { + Assert::assertTrue(false, $message ?: 'Not all elements satisfy the condition.'); + } + } + + Assert::assertTrue(true, $message); + + return $this; + } + + /** + * Asserts that at least one element in the array satisfies the given callback. + * + * This method checks if any element passes the condition defined by the callback. + * The callback receives the value and optionally the key. + * + * Example usage: + * fact([1, 2, 3])->some(fn($v) => $v > 2); // Passes + * fact([1, 2, 3])->some(fn($v) => $v > 10); // Fails + * + * @param callable $callback The function to test each element (receives value and key). + * @param string $message Optional custom error message. + * + * @return self Enables fluent chaining of assertion methods. + */ + public function some(callable $callback, string $message = ''): self + { + $array = $this->variable; + + if (!is_array($array)) { + Assert::assertTrue(false, $message ?: 'Variable is not an array.'); + } + + if (empty($array)) { + Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.'); + } + + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + Assert::assertTrue(true, $message); + + return $this; + } + } + + Assert::assertTrue(false, $message ?: 'No elements satisfy the condition.'); + } + + /** + * Asserts that no elements in the array satisfy the given callback. + * + * This method checks if none of the elements pass the condition defined by the callback. + * The callback receives the value and optionally the key. + * + * Example usage: + * fact([1, 2, 3])->none(fn($v) => $v > 10); // Passes + * fact([1, 2, 3])->none(fn($v) => $v > 2); // Fails + * + * @param callable $callback The function to test each element (receives value and key). + * @param string $message Optional custom error message. + * + * @return self Enables fluent chaining of assertion methods. + */ + public function none(callable $callback, string $message = ''): self + { + $array = $this->variable; + + if (!is_array($array)) { + Assert::assertTrue(false, $message ?: 'Variable is not an array.'); + } + + if (empty($array)) { + Assert::assertTrue(false, $message ?: 'Array is empty, cannot evaluate condition on elements.'); + } + + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + Assert::assertTrue(false, $message ?: 'At least one element satisfies the condition.'); + } + } + + Assert::assertTrue(true, $message); + + return $this; + } } \ No newline at end of file diff --git a/src/Traits/SpecialAssertions.php b/src/Traits/SpecialAssertions.php deleted file mode 100644 index dc33542..0000000 --- a/src/Traits/SpecialAssertions.php +++ /dev/null @@ -1,32 +0,0 @@ -ulid(); // Passes (if valid ULID) - * fact('invalid-ulid')->ulid(); // Fails - * - * @return self Enables fluent chaining of assertion methods. - */ - public function ulid(): self - { - return $this->matchesRegularExpression(RegularExpressionPattern::ULID); - } -} \ No newline at end of file diff --git a/src/Traits/StringAssertions.php b/src/Traits/StringAssertions.php index 7e41f73..00e078c 100644 --- a/src/Traits/StringAssertions.php +++ b/src/Traits/StringAssertions.php @@ -5,6 +5,7 @@ namespace K2gl\PHPUnitFluentAssertions\Traits; use K2gl\PHPUnitFluentAssertions\FluentAssertions; +use K2gl\PHPUnitFluentAssertions\Reference\RegularExpressionPattern; use PHPUnit\Framework\Assert; /** @@ -333,5 +334,25 @@ public function isValidEmail(string $message = ''): self return $this; } + + + /** + * Asserts that a variable is a valid ULID. + * + * This method checks if the actual value matches the ULID (Universally Unique Lexicographically Sortable Identifier) format. + * + * @see https://github.com/ulid/spec + * + * Example usage: + * fact('01ARZ3NDEKTSV4RRFFQ69G5FAV')->ulid(); // Passes (if valid ULID) + * fact('invalid-ulid')->ulid(); // Fails + * + * @return self Enables fluent chaining of assertion methods. + */ + public function ulid(): self + { + return $this->matchesRegularExpression(RegularExpressionPattern::ULID); + } + // endregion Length Methods } \ No newline at end of file diff --git a/src/Traits/TypeCheckingAssertions.php b/src/Traits/TypeCheckingAssertions.php index 1a01ae5..e07c866 100644 --- a/src/Traits/TypeCheckingAssertions.php +++ b/src/Traits/TypeCheckingAssertions.php @@ -195,4 +195,44 @@ public function isArray(string $message = ''): self return $this; } + + /** + * Asserts that a variable is of type resource. + * + * This method checks if the actual value is a resource. + * + * Example usage: + * fact(fopen('file.txt', 'r'))->isResource(); // Passes + * fact('string')->isResource(); // Fails + * + * @param string $message Optional custom error message. + * + * @return self Enables fluent chaining of assertion methods. + */ + public function isResource(string $message = ''): self + { + Assert::assertIsResource($this->variable, $message); + + return $this; + } + + /** + * Asserts that a variable is callable. + * + * This method checks if the actual value is callable (function, method, closure). + * + * Example usage: + * fact('strlen')->isCallable(); // Passes + * fact(123)->isCallable(); // Fails + * + * @param string $message Optional custom error message. + * + * @return self Enables fluent chaining of assertion methods. + */ + public function isCallable(string $message = ''): self + { + Assert::assertIsCallable($this->variable, $message); + + return $this; + } } \ No newline at end of file diff --git a/tests/FluentAssertions/Asserts/Every/EveryTest.php b/tests/FluentAssertions/Asserts/Every/EveryTest.php new file mode 100644 index 0000000..ac42781 --- /dev/null +++ b/tests/FluentAssertions/Asserts/Every/EveryTest.php @@ -0,0 +1,51 @@ +every($callback); + + // assert + $this->correctAssertionExecuted(); + } + + #[DataProvider('notEveryDataProvider')] + public function testNotEvery(mixed $variable, callable $callback): void + { + // assert + $this->incorrectAssertionExpected(); + + // act + fact($variable)->every($callback); + } + + public static function everyDataProvider(): array + { + return [ + [[2, 4, 6], fn($v) => $v % 2 === 0], + [['a' => 1, 'b' => 3], fn($v, $k) => $v > 0], + ]; + } + + public static function notEveryDataProvider(): array + { + return [ + [[1, 2, 3], fn($v) => $v > 5], + [[], fn($v) => true], // empty array + ['not array', fn($v) => true], // not array + ]; + } +} \ No newline at end of file diff --git a/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php b/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php new file mode 100644 index 0000000..a820513 --- /dev/null +++ b/tests/FluentAssertions/Asserts/IsCallable/IsCallableTest.php @@ -0,0 +1,53 @@ +isCallable(); + + // assert + $this->correctAssertionExecuted(); + } + + #[DataProvider('notIsCallableDataProvider')] + public function testNotIsCallable(mixed $variable): void + { + // assert + $this->incorrectAssertionExpected(); + + // act + fact($variable)->isCallable(); + } + + public static function isCallableDataProvider(): array + { + return [ + ['strlen'], + [fn() => true], + ]; + } + + public static function notIsCallableDataProvider(): array + { + return [ + ['string'], + [42], + [true], + [null], + [[]], + ]; + } +} \ No newline at end of file diff --git a/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php b/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php new file mode 100644 index 0000000..4724c75 --- /dev/null +++ b/tests/FluentAssertions/Asserts/IsResource/IsResourceTest.php @@ -0,0 +1,48 @@ +isResource(); + + // assert + $this->correctAssertionExecuted(); + + fclose($resource); + } + + #[DataProvider('notIsResourceDataProvider')] + public function testNotIsResource(mixed $variable): void + { + // assert + $this->incorrectAssertionExpected(); + + // act + fact($variable)->isResource(); + } + + public static function notIsResourceDataProvider(): array + { + return [ + ['string'], + [42], + [true], + [null], + [[]], + ]; + } +} \ No newline at end of file diff --git a/tests/FluentAssertions/Asserts/None/NoneTest.php b/tests/FluentAssertions/Asserts/None/NoneTest.php new file mode 100644 index 0000000..2f73e4b --- /dev/null +++ b/tests/FluentAssertions/Asserts/None/NoneTest.php @@ -0,0 +1,51 @@ +none($callback); + + // assert + $this->correctAssertionExecuted(); + } + + #[DataProvider('notNoneDataProvider')] + public function testNotNone(mixed $variable, callable $callback): void + { + // assert + $this->incorrectAssertionExpected(); + + // act + fact($variable)->none($callback); + } + + public static function noneDataProvider(): array + { + return [ + [[1, 2, 3], fn($v) => $v > 10], + [['a' => 1, 'b' => 2], fn($v, $k) => $k === 'c'], + ]; + } + + public static function notNoneDataProvider(): array + { + return [ + [[1, 2, 3], fn($v) => $v > 2], + [[], fn($v) => true], // empty array + ['not array', fn($v) => true], // not array + ]; + } +} \ No newline at end of file diff --git a/tests/FluentAssertions/Asserts/Some/SomeTest.php b/tests/FluentAssertions/Asserts/Some/SomeTest.php new file mode 100644 index 0000000..fec4385 --- /dev/null +++ b/tests/FluentAssertions/Asserts/Some/SomeTest.php @@ -0,0 +1,51 @@ +some($callback); + + // assert + $this->correctAssertionExecuted(); + } + + #[DataProvider('notSomeDataProvider')] + public function testNotSome(mixed $variable, callable $callback): void + { + // assert + $this->incorrectAssertionExpected(); + + // act + fact($variable)->some($callback); + } + + public static function someDataProvider(): array + { + return [ + [[1, 2, 3], fn($v) => $v > 2], + [['a' => 1, 'b' => 2], fn($v, $k) => $k === 'b'], + ]; + } + + public static function notSomeDataProvider(): array + { + return [ + [[1, 2, 3], fn($v) => $v > 10], + [[], fn($v) => true], // empty array + ['not array', fn($v) => true], // not array + ]; + } +} \ No newline at end of file