diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fceb541..d831084 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ "8.3", "8.4" ] + php: [ "8.3", "8.4", "8.5" ] dependencies: [ "lowest", "highest" ] steps: @@ -26,3 +26,20 @@ jobs: - run: vendor/bin/phpstan - run: vendor/bin/pest + + # Test actual pipe operator syntax on PHP 8.5 + pipe-operator-examples: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || contains(github.ref, 'pipe-operator') + + steps: + - uses: actions/checkout@v3 + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.5" + + - uses: ramsey/composer-install@v3 + + - name: Run pipe operator examples + run: php examples/pipe-operator.php diff --git a/PIPE_OPERATOR.md b/PIPE_OPERATOR.md new file mode 100644 index 0000000..54517e4 --- /dev/null +++ b/PIPE_OPERATOR.md @@ -0,0 +1,279 @@ +# PHP 8.5 Pipe Operator Support + +This document demonstrates how to use PHP 8.5's new pipe operator (`|>`) with the monads library. + +## What is the Pipe Operator? + +The pipe operator (`|>`) allows you to chain function calls in a more readable, left-to-right manner. Instead of deeply nested function calls or intermediate variables, you can write: + +```php +$result = $value + |> trim(...) + |> strtoupper(...) + |> fn($x) => str_replace(' ', '-', $x); +``` + +This is equivalent to: +```php +$result = str_replace(' ', '-', strtoupper(trim($value))); +``` + +## Using the Pipe Operator with Monads + +### Option Monad with Pipe Operator + +The library provides pipe-friendly helper functions in the `Superscript\Monads\Option\Pipe` namespace: + +```php +use function Superscript\Monads\Option\Pipe\{option, map, filter, andThen, unwrapOr}; + +// Process user input with validation +$username = $rawInput + |> option(...) // Wrap in Option + |> map(fn($x) => trim($x)) // Trim whitespace + |> filter(fn($x) => strlen($x) > 0) // Filter empty strings + |> map(fn($x) => strtolower($x)) // Convert to lowercase + |> unwrapOr('guest'); // Provide default + +// Safe array access +$users = [ + 1 => ['name' => 'Alice'], + 2 => ['name' => 'Bob'], +]; + +$userName = $userId + |> fn($id) => $users[$id] ?? null + |> option(...) + |> map(fn($u) => $u['name']) + |> unwrapOr('Unknown'); + +// Validation with flatMap +$validateAge = fn(?int $age) => + $age !== null && $age >= 18 && $age <= 100 + ? Some($age) + : None(); + +$result = $userAge + |> $validateAge(...) + |> map(fn($a) => "Age: $a years") + |> unwrapOr('Invalid age'); +``` + +### Result Monad with Pipe Operator + +The library provides pipe-friendly helper functions in the `Superscript\Monads\Result\Pipe` namespace: + +```php +use function Superscript\Monads\Result\Pipe\{toOk, map, andThen, mapErr, unwrapOr, matchResult}; + +// Validate and process numbers +$parseInt = fn(string $s): int => (int) $s; +$validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive"); +$double = fn(int $x): int => $x * 2; + +$result = $input + |> toOk(...) + |> map($parseInt) + |> andThen($validate) + |> map($double) + |> unwrapOr(0); + +// Safe division chain +$divide = fn(int $a, int $b) => + $b === 0 ? Err("Division by zero") : Ok($a / $b); + +$result = 100 + |> fn($x) => $divide($x, 2) + |> andThen(fn($x) => $divide((int)$x, 5)) + |> unwrapOr(0); + +// Error handling with match +$result = $value + |> toOk(...) + |> map(fn($x) => $x * 2) + |> matchResult( + fn($e) => "Error: $e", + fn($v) => "Success: $v" + ); + +// Parse JSON with validation +$parseJson = function (string $json) { + try { + return Ok(json_decode($json, true, 512, JSON_THROW_ON_ERROR)); + } catch (Exception $e) { + return Err("Invalid JSON"); + } +}; + +$validateUser = fn(array $data) => + isset($data['name']) && isset($data['email']) + ? Ok($data) + : Err("Missing required fields"); + +$extractName = fn(array $user): string => $user['name']; + +$userName = $jsonString + |> $parseJson(...) + |> andThen($validateUser) + |> map($extractName) + |> unwrapOr('Unknown'); +``` + +## Method Chaining (Works in PHP 8.3+) + +The monads already work great with the pipe operator because they support method chaining: + +```php +// Option chaining with pipe +$result = $value + |> Some(...) + |> fn($opt) => $opt->map(fn($x) => $x * 2) + |> fn($opt) => $opt->filter(fn($x) => $x > 10) + |> fn($opt) => $opt->unwrapOr(0); + +// Or using the traditional method chaining (PHP 8.3+) +$result = Some($value) + ->map(fn($x) => $x * 2) + ->filter(fn($x) => $x > 10) + ->unwrapOr(0); +``` + +## Real-World Examples + +### Example 1: Processing Form Input + +```php +use function Superscript\Monads\Option\Pipe\{option, map, filter, unwrapOr}; + +// Clean and validate email +$cleanEmail = $formData['email'] ?? null + |> option(...) + |> map(fn($e) => trim($e)) + |> filter(fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL)) + |> map(fn($e) => strtolower($e)) + |> unwrapOr(null); + +if ($cleanEmail === null) { + throw new ValidationException('Invalid email'); +} +``` + +### Example 2: Database Query with Error Handling + +```php +use function Superscript\Monads\Result\Pipe\{toOk, andThen, map, unwrapOr}; + +$findUser = fn(int $id) => + DB::find('users', $id) + ? Ok(DB::find('users', $id)) + : Err("User not found"); + +$validateUser = fn($user) => + $user['active'] + ? Ok($user) + : Err("User is inactive"); + +$formatUser = fn($user) => [ + 'id' => $user['id'], + 'name' => $user['name'], + 'email' => $user['email'], +]; + +$userData = $userId + |> $findUser(...) + |> andThen($validateUser) + |> map($formatUser) + |> unwrapOr(null); +``` + +### Example 3: Configuration Loading + +```php +use function Superscript\Monads\Result\Pipe\{toOk, map, andThen, matchResult}; + +$loadConfig = function(string $path) { + if (!file_exists($path)) { + return Err("Config file not found"); + } + + try { + $content = file_get_contents($path); + $config = json_decode($content, true, 512, JSON_THROW_ON_ERROR); + return Ok($config); + } catch (Exception $e) { + return Err("Invalid config format"); + } +}; + +$validateConfig = fn(array $config) => + isset($config['app_name']) && isset($config['version']) + ? Ok($config) + : Err("Missing required config keys"); + +$config = $configPath + |> $loadConfig(...) + |> andThen($validateConfig) + |> matchResult( + fn($err) => throw new ConfigException($err), + fn($cfg) => $cfg + ); +``` + +## Helper Functions Reference + +### Option Pipe Helpers + +- `option($value)` - Wrap value in Option (Some if non-null, None if null) +- `map($fn)` - Transform the contained value +- `filter($predicate)` - Filter based on predicate +- `andThen($fn)` - FlatMap operation +- `unwrapOr($default)` - Extract value or return default +- `isSomeAnd($predicate)` - Check if Some and satisfies predicate + +### Result Pipe Helpers + +- `toOk($value)` - Wrap value in Ok +- `toErr($error)` - Wrap error in Err +- `map($fn)` - Transform the success value +- `mapErr($fn)` - Transform the error value +- `andThen($fn)` - FlatMap operation +- `unwrapOr($default)` - Extract value or return default +- `matchResult($errFn, $okFn)` - Handle both cases +- `matchResult($result)($errFn, $okFn)` - Handle both cases + +## Why Pipe-Friendly Helpers? + +While the monads already have great method chaining APIs, the pipe-friendly helpers make it easier to compose operations in a functional style: + +1. **Clearer data flow** - Read operations left-to-right +2. **Better composition** - Easier to build reusable pipelines +3. **Functional style** - Matches the pipe operator's functional paradigm +4. **Point-free style** - Can pass functions directly without wrapping + +## Migration Guide + +If you're already using the library, you don't need to change anything! The traditional method chaining still works: + +```php +// Traditional (still works great) +$result = Some($value) + ->map(fn($x) => $x * 2) + ->filter(fn($x) => $x > 10) + ->unwrapOr(0); + +// With pipe operator (PHP 8.5+) +$result = $value + |> Some(...) + |> fn($opt) => $opt->map(fn($x) => $x * 2) + |> fn($opt) => $opt->filter(fn($x) => $x > 10) + |> fn($opt) => $opt->unwrapOr(0); + +// With pipe helpers (most functional) +$result = $value + |> option(...) + |> map(fn($x) => $x * 2) + |> filter(fn($x) => $x > 10) + |> unwrapOr(0); +``` + +Choose the style that best fits your codebase and preferences! diff --git a/README.md b/README.md index 371a24c..d228295 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,28 @@ composer require gosuperscript/monads ## Requirements - PHP 8.3 or higher +- PHP 8.5+ for pipe operator support (optional) ## Usage +### šŸš€ PHP 8.5 Pipe Operator Support + +This library fully supports PHP 8.5's new pipe operator (`|>`), enabling clean, functional-style data transformations: + +```php +use function Superscript\Monads\Option\Pipe\{option, map, filter, unwrapOr}; + +// Clean and readable data pipeline +$username = $userInput + |> option(...) // Wrap in Option + |> map(fn($x) => trim($x)) // Trim whitespace + |> filter(fn($x) => strlen($x) > 0) // Filter empty + |> map(fn($x) => strtolower($x)) // Lowercase + |> unwrapOr('guest'); // Default value +``` + +See [PIPE_OPERATOR.md](PIPE_OPERATOR.md) for comprehensive examples and migration guide. + ### Option Monad The `Option` type represents an optional value: every `Option` is either `Some` and contains a value, or `None`, and does not. This is a safer alternative to using `null`. diff --git a/composer.json b/composer.json index 675482b..72f5d69 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,9 @@ }, "files": [ "src/Result/functions.php", - "src/Option/functions.php" + "src/Option/functions.php", + "src/Result/pipe.php", + "src/Option/pipe.php" ] }, "autoload-dev": { @@ -17,7 +19,7 @@ } }, "require": { - "php": "^8.3" + "php": "^8.3 || ^8.4 || ^8.5" }, "require-dev": { "phpstan/phpstan": "^2.0", diff --git a/examples/pipe-operator.php b/examples/pipe-operator.php new file mode 100644 index 0000000..492a974 --- /dev/null +++ b/examples/pipe-operator.php @@ -0,0 +1,153 @@ +) in action. + */ + +require __DIR__ . '/../vendor/autoload.php'; + +use function Superscript\Monads\Option\{Some, None}; +use function Superscript\Monads\Option\Pipe\{option, map, filter, andThen, unwrapOr}; +use function Superscript\Monads\Result\{Ok, Err}; +use function Superscript\Monads\Result\Pipe\{toOk, matchResult}; +use function Superscript\Monads\Result\Pipe\map as resultMap; +use function Superscript\Monads\Result\Pipe\andThen as resultAndThen; + +// Check PHP version +if (PHP_VERSION_ID < 80500) { + echo "PHP 8.5+ is required to run pipe operator examples.\n"; + echo "Current version: " . PHP_VERSION . "\n"; + echo "\nThis file contains actual pipe operator syntax which will cause parse errors on PHP < 8.5.\n"; + exit(1); +} + +echo "āœ“ Running PHP 8.5 Pipe Operator Examples\n\n"; + +// Example 1: Option - String processing pipeline +echo "=== Example 1: String Processing with Option ===\n"; +$input = ' Hello World '; +$result = $input + |> option(...) + |> map(fn($x) => trim($x)) + |> map(fn($x) => strtolower($x)) + |> filter(fn($x) => strlen($x) > 5) + |> unwrapOr('default'); + +echo "Input: '{$input}'\n"; +echo "Result: '{$result}'\n\n"; + +// Example 2: Option - Null handling +echo "=== Example 2: Null Handling with Option ===\n"; +$nullInput = null; +$result = $nullInput + |> option(...) + |> map(fn($x) => $x * 2) + |> unwrapOr(99); + +echo "Input: null\n"; +echo "Result: {$result}\n\n"; + +// Example 3: Option - Value transformation +echo "=== Example 3: Value Transformation with Option ===\n"; +$number = 21; +$result = $number + |> option(...) + |> map(fn($x) => $x * 2) + |> filter(fn($x) => $x > 40) + |> unwrapOr(0); + +echo "Input: {$number}\n"; +echo "Result: {$result}\n\n"; + +// Example 4: Result - Safe division +echo "=== Example 4: Safe Division with Result ===\n"; +$divide = fn(int $a, int $b) => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + +$result1 = 100 + |> fn($x) => $divide($x, 2) + |> resultAndThen(fn($x) => $divide((int)$x, 5)) + |> unwrapOr(0); + +echo "100 / 2 / 5 = {$result1}\n"; + +$result2 = 100 + |> fn($x) => $divide($x, 0) + |> resultAndThen(fn($x) => $divide((int)$x, 5)) + |> unwrapOr(-1); + +echo "100 / 0 / 5 = {$result2} (error handled)\n\n"; + +// Example 5: Result - Number validation +echo "=== Example 5: Number Validation with Result ===\n"; +$parseInt = fn(string $s): int => (int) $s; +$validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive"); +$double = fn(int $x): int => $x * 2; + +$result = "5" + |> toOk(...) + |> resultMap($parseInt) + |> resultAndThen($validate) + |> resultMap($double) + |> unwrapOr(0); + +echo "Input: '5'\n"; +echo "Result after parse->validate->double: {$result}\n\n"; + +// Example 6: Result - Error handling with match +echo "=== Example 6: Error Handling with Match ===\n"; +$result = 42 + |> toOk(...) + |> resultMap(fn($x) => $x * 2) + |> matchResult( + fn($e) => "Error: $e", + fn($v) => "Value: $v" + ); + +echo "Result: {$result}\n\n"; + +// Example 7: Option - FlatMap with andThen +echo "=== Example 7: FlatMap with andThen ===\n"; +$validatePositive = fn($x) => $x > 0 ? Some($x) : None(); + +$result1 = 42 + |> option(...) + |> andThen($validatePositive) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + +$result2 = -5 + |> option(...) + |> andThen($validatePositive) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + +echo "42 validated and doubled: {$result1}\n"; +echo "-5 validated and doubled: {$result2} (failed validation)\n\n"; + +// Example 8: Clean user input +echo "=== Example 8: Clean User Input ===\n"; +$rawInputs = [' john ', ' ', null, 'ALICE']; + +foreach ($rawInputs as $input) { + $username = $input + |> option(...) + |> map(fn($x) => trim($x)) + |> filter(fn($x) => strlen($x) > 0) + |> map(fn($x) => strtolower($x)) + |> unwrapOr('guest'); + + echo sprintf("Input: %s -> Username: %s\n", var_export($input, true), $username); +} + +echo "\nāœ“ All examples completed successfully!\n"; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..b4c9327 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,115 @@ +parameters: + ignoreErrors: + - + message: '#^Function Superscript\\Monads\\Option\\Pipe\\filter\(\) should return callable\(Superscript\\Monads\\Option\\Option\\)\: Superscript\\Monads\\Option\\Option\ but returns Closure\(Superscript\\Monads\\Option\\Option\)\: Superscript\\Monads\\Option\\Option\\.$#' + identifier: return.type + count: 1 + path: src/Option/pipe.php + + - + message: '#^Parameter \#1 \$f of method Superscript\\Monads\\Option\\Option\\:\:andThen\(\) expects callable\(mixed\)\: Superscript\\Monads\\Option\\Option\, callable\(T\)\: Superscript\\Monads\\Option\\Option\ given\.$#' + identifier: argument.type + count: 1 + path: src/Option/pipe.php + + - + message: '#^Parameter \#1 \$f of method Superscript\\Monads\\Option\\Option\\:\:filter\(\) expects callable\(mixed\)\: bool, callable\(T\)\: bool given\.$#' + identifier: argument.type + count: 1 + path: src/Option/pipe.php + + - + message: '#^Parameter \#1 \$f of method Superscript\\Monads\\Option\\Option\\:\:isSomeAnd\(\) expects callable\(mixed\)\: bool, callable\(T\)\: bool given\.$#' + identifier: argument.type + count: 1 + path: src/Option/pipe.php + + - + message: '#^Parameter \#1 \$f of method Superscript\\Monads\\Option\\Option\\:\:map\(\) expects callable\(mixed\)\: U, callable\(T\)\: U given\.$#' + identifier: argument.type + count: 1 + path: src/Option/pipe.php + + - + message: '#^Template type T of function Superscript\\Monads\\Option\\Pipe\\unwrapOr\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Option/pipe.php + + - + message: '#^Function Superscript\\Monads\\Result\\Pipe\\andThen\(\) should return callable\(Superscript\\Monads\\Result\\Result\\)\: Superscript\\Monads\\Result\\Result\ but returns Closure\(Superscript\\Monads\\Result\\Result\)\: Superscript\\Monads\\Result\\Result\\.$#' + identifier: return.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Function Superscript\\Monads\\Result\\Pipe\\map\(\) should return callable\(Superscript\\Monads\\Result\\Result\\)\: Superscript\\Monads\\Result\\Result\ but returns Closure\(Superscript\\Monads\\Result\\Result\)\: Superscript\\Monads\\Result\\Result\\.$#' + identifier: return.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Function Superscript\\Monads\\Result\\Pipe\\mapErr\(\) should return callable\(Superscript\\Monads\\Result\\Result\\)\: Superscript\\Monads\\Result\\Result\ but returns Closure\(Superscript\\Monads\\Result\\Result\)\: Superscript\\Monads\\Result\\Result\\.$#' + identifier: return.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Parameter \#1 \$err of method Superscript\\Monads\\Result\\Result\\:\:match\(\) expects callable\(mixed\)\: U, callable\(E\)\: U given\.$#' + identifier: argument.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Parameter \#1 \$op of method Superscript\\Monads\\Result\\Result\\:\:map\(\) expects callable\(mixed\)\: U, callable\(T\)\: U given\.$#' + identifier: argument.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Parameter \#1 \$op of method Superscript\\Monads\\Result\\Result\\:\:mapErr\(\) expects callable\(mixed\)\: F, callable\(E\)\: F given\.$#' + identifier: argument.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Parameter \#1 \$other of method Superscript\\Monads\\Result\\Result\\:\:andThen\(\) expects callable\(mixed\)\: Superscript\\Monads\\Result\\Result\, callable\(T\)\: Superscript\\Monads\\Result\\Result\ given\.$#' + identifier: argument.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Parameter \#2 \$ok of method Superscript\\Monads\\Result\\Result\\:\:match\(\) expects callable\(mixed\)\: U, callable\(T\)\: U given\.$#' + identifier: argument.type + count: 1 + path: src/Result/pipe.php + + - + message: '#^Template type E of function Superscript\\Monads\\Result\\Pipe\\andThen\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Result/pipe.php + + - + message: '#^Template type E of function Superscript\\Monads\\Result\\Pipe\\map\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Result/pipe.php + + - + message: '#^Template type E of function Superscript\\Monads\\Result\\Pipe\\unwrapOr\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Result/pipe.php + + - + message: '#^Template type T of function Superscript\\Monads\\Result\\Pipe\\mapErr\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Result/pipe.php + + - + message: '#^Template type T of function Superscript\\Monads\\Result\\Pipe\\unwrapOr\(\) is not referenced in a parameter\.$#' + identifier: method.templateTypeNotInParameter + count: 1 + path: src/Result/pipe.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 961fd4f..4a66763 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - phpstan-baseline.neon + parameters: level: max diff --git a/pint.json b/pint.json index 5ad32f4..a715a31 100644 --- a/pint.json +++ b/pint.json @@ -1,3 +1,6 @@ { - "preset": "per" + "preset": "per", + "exclude": [ + "examples" + ] } diff --git a/src/Option/None.php b/src/Option/None.php index c351f6c..376457e 100644 --- a/src/Option/None.php +++ b/src/Option/None.php @@ -6,7 +6,6 @@ use RuntimeException; use Superscript\Monads\Result\Result; - use Throwable; use function Superscript\Monads\Result\Err; diff --git a/src/Option/Some.php b/src/Option/Some.php index 45e4c48..91ad48b 100644 --- a/src/Option/Some.php +++ b/src/Option/Some.php @@ -132,6 +132,6 @@ public function transpose(): Result throw new InvalidArgumentException('Cannot transpose a Some value that is not a Result'); } - return $this->value->andThen(fn ($value) => Ok(new self($value))); + return $this->value->andThen(fn($value) => Ok(new self($value))); } } diff --git a/src/Option/pipe.php b/src/Option/pipe.php new file mode 100644 index 0000000..e79f856 --- /dev/null +++ b/src/Option/pipe.php @@ -0,0 +1,91 @@ + + */ +function option(mixed $value): Option +{ + return Option::from($value); +} + +/** + * Map over an Option value. + * Pipe-friendly helper for chaining transformations. + * + * @template T + * @template U + * @param callable(T): U $f + * @return callable(Option): Option + */ +function map(callable $f): callable +{ + return fn(Option $option): Option => $option->map($f); +} + +/** + * Filter an Option value. + * Pipe-friendly helper for conditional filtering. + * + * @template T + * @param callable(T): bool $predicate + * @return callable(Option): Option + */ +function filter(callable $predicate): callable +{ + return fn(Option $option): Option => $option->filter($predicate); +} + +/** + * FlatMap over an Option value. + * Pipe-friendly helper for chaining operations that return Options. + * + * @template T + * @template U + * @param callable(T): Option $f + * @return callable(Option): Option + */ +function andThen(callable $f): callable +{ + return fn(Option $option): Option => $option->andThen($f); +} + +/** + * Unwrap an Option with a default value. + * Pipe-friendly helper for extracting values safely. + * + * @template T + * @template U + * @param U $default + * @return callable(Option): (T|U) + */ +function unwrapOr(mixed $default): callable +{ + return fn(Option $option): mixed => $option->unwrapOr($default); +} + +/** + * Check if Option is Some and satisfies predicate. + * Pipe-friendly helper for conditional checks. + * + * @template T + * @param callable(T): bool $predicate + * @return callable(Option): bool + */ +function isSomeAnd(callable $predicate): callable +{ + return fn(Option $option): bool => $option->isSomeAnd($predicate); +} diff --git a/src/Result/Err.php b/src/Result/Err.php index 257db0d..85cd219 100644 --- a/src/Result/Err.php +++ b/src/Result/Err.php @@ -9,6 +9,7 @@ use Superscript\Monads\Option\Option; use Superscript\Monads\Option\Some; use Throwable; + use function Superscript\Monads\Option\Some; /** diff --git a/src/Result/Ok.php b/src/Result/Ok.php index 83f61bb..9caa5dc 100644 --- a/src/Result/Ok.php +++ b/src/Result/Ok.php @@ -37,7 +37,7 @@ public function andThen(callable $other): Result public function err(): Option { - return new None; + return new None(); } public function expect(string|Throwable $message): mixed @@ -146,6 +146,6 @@ public function transpose(): Option throw new InvalidArgumentException('Cannot transpose an Ok value that is not an Option'); } - return $this->value->map(fn ($value) => new self($value)); + return $this->value->map(fn($value) => new self($value)); } } diff --git a/src/Result/pipe.php b/src/Result/pipe.php new file mode 100644 index 0000000..b50b873 --- /dev/null +++ b/src/Result/pipe.php @@ -0,0 +1,113 @@ + + */ +function toOk(mixed $value): Result +{ + return Ok($value); +} + +/** + * Wrap an error in Err. + * Pipe-friendly version for creating error results. + * + * @template E + * @param E $error + * @return Result + */ +function toErr(mixed $error): Result +{ + return Err($error); +} + +/** + * Map over a Result value. + * Pipe-friendly helper for chaining transformations. + * + * @template T + * @template E + * @template U + * @param callable(T): U $f + * @return callable(Result): Result + */ +function map(callable $f): callable +{ + return fn(Result $result): Result => $result->map($f); +} + +/** + * Map over a Result error. + * Pipe-friendly helper for transforming errors. + * + * @template T + * @template E + * @template F + * @param callable(E): F $f + * @return callable(Result): Result + */ +function mapErr(callable $f): callable +{ + return fn(Result $result): Result => $result->mapErr($f); +} + +/** + * FlatMap over a Result value. + * Pipe-friendly helper for chaining operations that return Results. + * + * @template T + * @template E + * @template U + * @template F + * @param callable(T): Result $f + * @return callable(Result): Result + */ +function andThen(callable $f): callable +{ + return fn(Result $result): Result => $result->andThen($f); +} + +/** + * Unwrap a Result with a default value. + * Pipe-friendly helper for extracting values safely. + * + * @template T + * @template E + * @template U + * @param U $default + * @return callable(Result): (T|U) + */ +function unwrapOr(mixed $default): callable +{ + return fn(Result $result): mixed => $result->unwrapOr($default); +} + +/** + * Match on a Result value. + * Pipe-friendly helper for handling both Ok and Err cases. + * + * @template T + * @template E + * @template U + * @param callable(E): U $err + * @param callable(T): U $ok + * @return callable(Result): U + */ +function matchResult(callable $err, callable $ok): callable +{ + return fn(Result $result): mixed => $result->match($err, $ok); +} diff --git a/tests/Lazy/LazyTypeTest.php b/tests/Lazy/LazyTypeTest.php index 9c8e342..f54070a 100644 --- a/tests/Lazy/LazyTypeTest.php +++ b/tests/Lazy/LazyTypeTest.php @@ -22,7 +22,7 @@ public static function providesTypeAssertions(): array public function testFileAsserts( string $assertType, string $file, - mixed ...$args + mixed ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); } diff --git a/tests/Option/OptionTest.php b/tests/Option/OptionTest.php index 6abbd08..3cefd41 100644 --- a/tests/Option/OptionTest.php +++ b/tests/Option/OptionTest.php @@ -4,7 +4,6 @@ use Superscript\Monads\Option\CannotUnwrapNone; use Superscript\Monads\Option\Option; - use Superscript\Monads\Result\Result; use function Superscript\Monads\Option\None; @@ -187,7 +186,7 @@ ]); test('transpose with non-result', function (Option $option) { - expect(fn () => $option->transpose())->toThrow(new InvalidArgumentException('Cannot transpose a Some value that is not a Result')); + expect(fn() => $option->transpose())->toThrow(new InvalidArgumentException('Cannot transpose a Some value that is not a Result')); })->with([ [Some(2), Some(2)], ]); diff --git a/tests/Option/OptionTypeTest.php b/tests/Option/OptionTypeTest.php index 3fa069b..7c2585c 100644 --- a/tests/Option/OptionTypeTest.php +++ b/tests/Option/OptionTypeTest.php @@ -22,7 +22,7 @@ public static function providesTypeAssertions(): array public function testFileAsserts( string $assertType, string $file, - mixed ...$args + mixed ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); } diff --git a/tests/Option/Php85PipeOperatorTest.php85 b/tests/Option/Php85PipeOperatorTest.php85 new file mode 100644 index 0000000..021d4c7 --- /dev/null +++ b/tests/Option/Php85PipeOperatorTest.php85 @@ -0,0 +1,127 @@ + syntax. + * + * This file contains actual pipe operator syntax and will only be loaded on PHP 8.5+. + * See tests/php85-bootstrap.php for the conditional loading logic. + */ + +test('pipe operator - option creation and basic map', function () { + $result = 21 + |> option(...) + |> map(fn($x) => $x * 2); + + expect($result)->toEqual(Some(42)); +}); + +test('pipe operator - null handling', function () { + $result = null + |> option(...) + |> map(fn($x) => $x * 2) + |> unwrapOr(99); + + expect($result)->toBe(99); +}); + +test('pipe operator - full chain with filter', function () { + $result = 10 + |> option(...) + |> map(fn($x) => $x * 2) + |> filter(fn($x) => $x > 15) + |> unwrapOr(0); + + expect($result)->toBe(20); +}); + +test('pipe operator - filter removes value', function () { + $result = 5 + |> option(...) + |> filter(fn($x) => $x > 10) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + + expect($result)->toBe(0); +}); + +test('pipe operator - string processing pipeline', function () { + $result = ' Hello World ' + |> option(...) + |> map(fn($x) => trim($x)) + |> map(fn($x) => strtolower($x)) + |> filter(fn($x) => strlen($x) > 5) + |> unwrapOr('default'); + + expect($result)->toBe('hello world'); +}); + +test('pipe operator - string processing with empty result', function () { + $result = ' ' + |> option(...) + |> map(fn($x) => trim($x)) + |> filter(fn($x) => strlen($x) > 0) + |> unwrapOr('guest'); + + expect($result)->toBe('guest'); +}); + +test('pipe operator - flatMap with andThen success case', function () { + $validatePositive = fn($x) => $x > 0 ? Some($x) : None(); + + $result = 42 + |> option(...) + |> andThen($validatePositive) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + + expect($result)->toBe(84); +}); + +test('pipe operator - flatMap with andThen failure case', function () { + $validatePositive = fn($x) => $x > 0 ? Some($x) : None(); + + $result = -5 + |> option(...) + |> andThen($validatePositive) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + + expect($result)->toBe(0); +}); + +test('pipe operator - complex user input processing', function () { + $rawInputs = [' john ', ' ', null, 'ALICE']; + $expected = ['john', 'guest', 'guest', 'alice']; + + foreach ($rawInputs as $index => $input) { + $username = $input + |> option(...) + |> map(fn($x) => trim($x)) + |> filter(fn($x) => strlen($x) > 0) + |> map(fn($x) => strtolower($x)) + |> unwrapOr('guest'); + + expect($username)->toBe($expected[$index]); + } +}); + +test('pipe operator - multiple transformations', function () { + $result = 5 + |> option(...) + |> map(fn($x) => $x + 5) + |> map(fn($x) => $x * 2) + |> map(fn($x) => $x - 10) + |> unwrapOr(0); + + expect($result)->toBe(10); +}); diff --git a/tests/Option/PipeTest.php b/tests/Option/PipeTest.php new file mode 100644 index 0000000..5084e07 --- /dev/null +++ b/tests/Option/PipeTest.php @@ -0,0 +1,157 @@ + option(...) + * |> map(fn($x) => $x * 2) + * |> filter(fn($x) => $x > 10) + * |> unwrapOr(0); + */ + +test('pipe friendly option creation from value', function () { + expect(option(42))->toEqual(Some(42)); + expect(option(null))->toEqual(None()); +}); + +test('pipe friendly map transformation', function () { + $result = map(fn($x) => $x * 2)(Some(21)); + expect($result)->toEqual(Some(42)); + + $result = map(fn($x) => $x * 2)(None()); + expect($result)->toEqual(None()); +}); + +test('pipe friendly filter operation', function () { + $result = filter(fn($x) => $x > 40)(Some(42)); + expect($result)->toEqual(Some(42)); + + $result = filter(fn($x) => $x > 40)(Some(5)); + expect($result)->toEqual(None()); + + $result = filter(fn($x) => $x > 40)(None()); + expect($result)->toEqual(None()); +}); + +test('pipe friendly flatMap operation', function () { + $result = andThen(fn($x) => $x > 40 ? Some($x * 2) : None())(Some(42)); + expect($result)->toEqual(Some(84)); + + $result = andThen(fn($x) => $x > 40 ? Some($x * 2) : None())(Some(5)); + expect($result)->toEqual(None()); +}); + +test('pipe friendly unwrapOr operation', function () { + $result = unwrapOr(0)(Some(42)); + expect($result)->toBe(42); + + $result = unwrapOr(0)(None()); + expect($result)->toBe(0); +}); + +test('pipe chain example - process user input', function () { + // Simulate: $input |> option(...) |> map(trim) |> filter(notEmpty) |> map(strtoupper) |> unwrapOr('GUEST') + $input = ' john '; + + $result = unwrapOr('GUEST')( + map(fn($x) => strtoupper($x))( + filter(fn($x) => strlen($x) > 0)( + map(fn($x) => trim($x))(option($input)) + ) + ) + ); + + expect($result)->toBe('JOHN'); +}); + +test('pipe chain example - empty input returns default', function () { + $input = ' '; + + $result = unwrapOr('GUEST')( + map(fn($x) => strtoupper($x))( + filter(fn($x) => strlen($x) > 0)( + map(fn($x) => trim($x))(option($input)) + ) + ) + ); + + expect($result)->toBe('GUEST'); +}); + +test('pipe chain example - null input returns default', function () { + $input = null; + + $result = unwrapOr('GUEST')( + map(fn($x) => strtoupper($x))( + filter(fn($x) => strlen($x) > 0)( + map(fn($x) => trim($x))(option($input)) + ) + ) + ); + + expect($result)->toBe('GUEST'); +}); + +test('pipe friendly chaining with method calls', function () { + // This demonstrates how the existing methods work perfectly with pipe operator + // With PHP 8.5: $value |> Some(...) |> map(...) |> filter(...) |> unwrapOr(...) + + $result = Some(10) + ->map(fn($x) => $x * 2) + ->filter(fn($x) => $x > 15) + ->map(fn($x) => "Value: $x") + ->unwrapOr("No value"); + + expect($result)->toBe("Value: 20"); +}); + +test('pipe operator style - safe array access', function () { + $users = [ + 1 => ['id' => 1, 'name' => 'Alice'], + 2 => ['id' => 2, 'name' => 'Bob'], + ]; + + // With PHP 8.5 pipe: $id |> fn($x) => $users[$x] ?? null |> option(...) |> map(fn($u) => $u['name']) |> unwrapOr('Unknown') + $getUserName = fn(int $id) => unwrapOr('Unknown')( + map(fn($u) => $u['name'])(option($users[$id] ?? null)) + ); + + expect($getUserName(1))->toBe('Alice'); + expect($getUserName(99))->toBe('Unknown'); +}); + +test('pipe operator style - validate and transform', function () { + $validateAge = fn(?int $age) => $age !== null && $age >= 18 && $age <= 100 + ? Some($age) + : None(); + + // With PHP 8.5: $input |> $validateAge(...) |> map(fn($a) => "Age: $a") |> unwrapOr('Invalid age') + $result = unwrapOr('Invalid age')( + map(fn($a) => "Age: $a")($validateAge(25)) + ); + + expect($result)->toBe('Age: 25'); + + $result = unwrapOr('Invalid age')( + map(fn($a) => "Age: $a")($validateAge(200)) + ); + + expect($result)->toBe('Invalid age'); +}); diff --git a/tests/Option/types.php b/tests/Option/types.php index 170c5b3..5782676 100644 --- a/tests/Option/types.php +++ b/tests/Option/types.php @@ -66,4 +66,4 @@ assertType(Option::class . '>', Option::collect($items)); /** @var Option> $x */ -assertType(Result::class . '<'.Option::class.', '.Throwable::class.'>', $x->transpose()); \ No newline at end of file +assertType(Result::class . '<' . Option::class . ', ' . Throwable::class . '>', $x->transpose()); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..41b1127 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,59 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +// expect()->extend('toBeOne', function () { +// return $this->toBe(1); +// }); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +// function something() +// { +// // .. +// } + +/* +|-------------------------------------------------------------------------- +| PHP 8.5 Pipe Operator Tests +|-------------------------------------------------------------------------- +| +| Load PHP 8.5-specific tests that contain pipe operator syntax. +| These are conditionally loaded only on PHP 8.5+ to prevent parse errors. +| +*/ + +require_once __DIR__ . '/php85-bootstrap.php'; diff --git a/tests/Result/Php85PipeOperatorTest.php85 b/tests/Result/Php85PipeOperatorTest.php85 new file mode 100644 index 0000000..041c343 --- /dev/null +++ b/tests/Result/Php85PipeOperatorTest.php85 @@ -0,0 +1,162 @@ + syntax. + * + * This file contains actual pipe operator syntax and will only be loaded on PHP 8.5+. + * See tests/php85-bootstrap.php for the conditional loading logic. + */ + +test('pipe operator - toOk and basic map', function () { + $result = 21 + |> toOk(...) + |> map(fn($x) => $x * 2); + + expect($result)->toEqual(Ok(42)); +}); + +test('pipe operator - full success chain', function () { + $result = 10 + |> toOk(...) + |> map(fn($x) => $x * 2) + |> map(fn($x) => $x + 5) + |> unwrapOr(0); + + expect($result)->toBe(25); +}); + +test('pipe operator - error handling with mapErr', function () { + $divide = fn(int $a, int $b) => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + + $result = $divide(10, 0) + |> mapErr(fn($e) => "Error: $e") + |> unwrapOr(-1); + + expect($result)->toBe(-1); +}); + +test('pipe operator - safe division success chain', function () { + $divide = fn(int $a, int $b) => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + + $result = 100 + |> fn($x) => $divide($x, 2) + |> andThen(fn($x) => $divide((int)$x, 5)) + |> unwrapOr(0); + + expect($result)->toBe(10); +}); + +test('pipe operator - safe division error propagation', function () { + $divide = fn(int $a, int $b) => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + + $result = 100 + |> fn($x) => $divide($x, 0) + |> andThen(fn($x) => $divide((int)$x, 5)) + |> unwrapOr(-1); + + expect($result)->toBe(-1); +}); + +test('pipe operator - matchResult with success', function () { + $result = 42 + |> toOk(...) + |> map(fn($x) => $x * 2) + |> matchResult( + fn($e) => "Error: $e", + fn($v) => "Value: $v" + ); + + expect($result)->toBe('Value: 84'); +}); + +test('pipe operator - matchResult with error', function () { + $result = "test error" + |> toErr(...) + |> matchResult( + fn($e) => "Error: $e", + fn($v) => "Value: $v" + ); + + expect($result)->toBe('Error: test error'); +}); + +test('pipe operator - validation pipeline success', function () { + $validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive"); + + $result = -5 + |> $validate(...) + |> map(fn($x) => $x * 2) + |> unwrapOr(0); + + expect($result)->toBe(0); +}); + +test('pipe operator - number validation and transformation', function () { + $parseInt = fn(string $s): int => (int) $s; + $validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive"); + $double = fn(int $x): int => $x * 2; + + $result = "5" + |> toOk(...) + |> map($parseInt) + |> andThen($validate) + |> map($double) + |> unwrapOr(0); + + expect($result)->toBe(10); +}); + +test('pipe operator - number validation failure', function () { + $parseInt = fn(string $s): int => (int) $s; + $validate = fn(int $x) => $x > 0 ? Ok($x) : Err("Must be positive"); + $double = fn(int $x): int => $x * 2; + + $result = "-5" + |> toOk(...) + |> map($parseInt) + |> andThen($validate) + |> map($double) + |> unwrapOr(0); + + expect($result)->toBe(0); +}); + +test('pipe operator - multiple error transformations', function () { + $result = "initial error" + |> toErr(...) + |> mapErr(fn($e) => "Wrapped: $e") + |> mapErr(fn($e) => strtoupper($e)) + |> unwrapOr("default"); + + expect($result)->toBe("default"); +}); + +test('pipe operator - complex calculation pipeline', function () { + $result = 5 + |> toOk(...) + |> map(fn($x) => $x + 3) + |> map(fn($x) => $x * 2) + |> andThen(fn($x) => $x > 10 ? Ok($x) : Err("too small")) + |> map(fn($x) => $x - 6) + |> unwrapOr(0); + + expect($result)->toBe(10); +}); diff --git a/tests/Result/PipeTest.php b/tests/Result/PipeTest.php new file mode 100644 index 0000000..f4a3013 --- /dev/null +++ b/tests/Result/PipeTest.php @@ -0,0 +1,203 @@ + toOk(...) + * |> map(fn($x) => $x * 2) + * |> andThen(validateValue) + * |> unwrapOr(0); + */ + +test('pipe friendly ok creation', function () { + expect(toOk(42))->toEqual(Ok(42)); +}); + +test('pipe friendly err creation', function () { + expect(toErr('error'))->toEqual(Err('error')); +}); + +test('pipe friendly map transformation', function () { + $result = map(fn($x) => $x * 2)(Ok(21)); + expect($result)->toEqual(Ok(42)); + + $result = map(fn($x) => $x * 2)(Err('error')); + expect($result)->toEqual(Err('error')); +}); + +test('pipe friendly mapErr transformation', function () { + $result = mapErr(fn($e) => "Wrapped: $e")(Ok(42)); + expect($result)->toEqual(Ok(42)); + + $result = mapErr(fn($e) => "Wrapped: $e")(Err('error')); + expect($result)->toEqual(Err('Wrapped: error')); +}); + +test('pipe friendly flatMap operation', function () { + $divide = fn(int $a, int $b): mixed => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + + $result = andThen(fn($x) => $divide($x, 2))(Ok(10)); + expect($result)->toEqual(Ok(5)); + + $result = andThen(fn($x) => $divide($x, 0))(Ok(10)); + expect($result)->toEqual(Err("Division by zero")); + + $result = andThen(fn($x) => $divide($x, 2))(Err('previous error')); + expect($result)->toEqual(Err('previous error')); +}); + +test('pipe friendly unwrapOr operation', function () { + $result = unwrapOr(0)(Ok(42)); + expect($result)->toBe(42); + + $result = unwrapOr(0)(Err('error')); + expect($result)->toBe(0); +}); + +test('pipe friendly match operation', function () { + $result = matchResult( + fn($e) => "Error: $e", + fn($v) => "Value: $v", + )(Ok(42)); + expect($result)->toBe('Value: 42'); + + $result = matchResult( + fn($e) => "Error: $e", + fn($v) => "Value: $v", + )(Err('oops')); + expect($result)->toBe('Error: oops'); +}); + +test('pipe chain example - validate and process number', function () { + // Simulate: $input |> toOk(...) |> map(parseInt) |> andThen(validate) |> map(double) |> unwrapOr(0) + + $parseInt = fn(string $s): int => (int) $s; + $validate = fn(int $x): mixed => $x > 0 ? Ok($x) : Err("Must be positive"); + $double = fn(int $x): int => $x * 2; + + $process = fn(string $input) => unwrapOr(0)( + map($double)( + andThen($validate)( + map($parseInt)(toOk($input)) + ) + ) + ); + + expect($process("5"))->toBe(10); + expect($process("-5"))->toBe(0); + expect($process("0"))->toBe(0); +}); + +test('pipe operator style - safe division chain', function () { + $divide = fn(int $a, int $b): mixed => $b === 0 + ? Err("Division by zero") + : Ok($a / $b); + + // With PHP 8.5: 100 |> fn($x) => $divide($x, 2) |> andThen(fn($x) => $divide((int)$x, 5)) |> unwrapOr(0) + $result = unwrapOr(0)( + andThen(fn($x) => $divide((int) $x, 5))( + $divide(100, 2) + ) + ); + + expect($result)->toBe(10); + + // Test with division by zero + $result = unwrapOr(0)( + andThen(fn($x) => $divide((int) $x, 5))( + $divide(100, 0) + ) + ); + + expect($result)->toBe(0); +}); + +test('pipe operator style - error recovery', function () { + // With PHP 8.5: $value |> toOk(...) |> map(risky) |> mapErr(recover) |> unwrapOr('fallback') + + $risky = fn(int $x): int => $x < 0 ? throw new Exception("Negative!") : $x * 2; + $recover = fn($e): string => "Recovered from error"; + + $result = unwrapOr('fallback')( + mapErr($recover)( + map($risky)(Ok(5)) + ) + ); + + expect($result)->toBe(10); +}); + +test('pipe friendly chaining with method calls', function () { + // This demonstrates how the existing methods work perfectly with pipe operator + // With PHP 8.5: $value |> Ok(...) |> map(...) |> andThen(...) |> unwrapOr(...) + + $result = Ok(10) + ->map(fn($x) => $x * 2) + ->andThen(fn($x) => $x > 15 ? Ok($x) : Err("too small")) + ->map(fn($x) => "Result: $x") + ->unwrapOr("Failed"); + + expect($result)->toBe("Result: 20"); + + $result = Ok(5) + ->map(fn($x) => $x * 2) + ->andThen(fn($x) => $x > 15 ? Ok($x) : Err("too small")) + ->map(fn($x) => "Result: $x") + ->unwrapOr("Failed"); + + expect($result)->toBe("Failed"); +}); + +test('pipe operator style - parse and validate JSON', function () { + $parseJson = function (string $json): mixed { + try { + return Ok(json_decode($json, true, 512, JSON_THROW_ON_ERROR)); + } catch (Exception $e) { + return Err("Invalid JSON"); + } + }; + + $validateUser = fn(array $data): mixed + => isset($data['name']) && isset($data['email']) + ? Ok($data) + : Err("Missing required fields"); + + $extractName = fn(array $user): string => $user['name']; + + // With PHP 8.5: $json |> $parseJson(...) |> andThen(validateUser) |> map(extractName) |> unwrapOr('Unknown') + $process = fn(string $json) => unwrapOr('Unknown')( + map($extractName)( + andThen($validateUser)($parseJson($json)) + ) + ); + + $validJson = '{"name":"Alice","email":"alice@example.com"}'; + expect($process($validJson))->toBe('Alice'); + + $invalidJson = '{"name":"Bob"}'; + expect($process($invalidJson))->toBe('Unknown'); + + $malformedJson = '{invalid}'; + expect($process($malformedJson))->toBe('Unknown'); +}); diff --git a/tests/Result/ResultTest.php b/tests/Result/ResultTest.php index 93b0ce0..2e17837 100644 --- a/tests/Result/ResultTest.php +++ b/tests/Result/ResultTest.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Superscript\Monads\Option\Option; use Superscript\Monads\Result\CannotUnwrapErr; use Superscript\Monads\Result\Err; use Superscript\Monads\Result\Ok; @@ -192,21 +191,21 @@ expect($result->unwrapOr($other))->toEqual($expected); })->with([ [Ok(9), 2, 9], - [Err('err'), 2, 2] + [Err('err'), 2, 2], ]); test('unwrap or else', function (Result $result, callable $op, mixed $expected) { expect($result->unwrapOrElse($op))->toEqual($expected); })->with([ [Ok(2), strlen(...), 2], - [Err('foo'), strlen(...), 3] + [Err('foo'), strlen(...), 3], ]); test('unwrap either', function (Result $result, mixed $expected) { expect($result->unwrapEither())->toEqual($expected); })->with([ [Ok(42), 42], - [Err('foo'), 'foo'] + [Err('foo'), 'foo'], ]); test('into ok', function (Ok $ok, mixed $expected) { @@ -225,7 +224,7 @@ expect(Result::collect($items))->toEqual($expected); })->with([ [[Ok(1), Ok(2)], Ok([1, 2])], - [[Err('error')], Err('error')] + [[Err('error')], Err('error')], ]); test('attempt', function () { diff --git a/tests/Result/ResultTypeTest.php b/tests/Result/ResultTypeTest.php index af12ad4..ca0a7df 100644 --- a/tests/Result/ResultTypeTest.php +++ b/tests/Result/ResultTypeTest.php @@ -22,7 +22,7 @@ public static function providesTypeAssertions(): array public function testFileAsserts( string $assertType, string $file, - mixed ...$args + mixed ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); } diff --git a/tests/Result/Testing/AssertionsTest.php b/tests/Result/Testing/AssertionsTest.php index 8840f1f..3aee4ca 100644 --- a/tests/Result/Testing/AssertionsTest.php +++ b/tests/Result/Testing/AssertionsTest.php @@ -16,15 +16,15 @@ class ResultAssertionsTestCase extends TestCase test('assertErr passes with Err result', function () { $errResult = Err('error message'); - + ResultAssertionsTestCase::assertErr($errResult); - + expect(true)->toBeTrue(); // If we get here, the assertion passed }); test('assertErr fails with Ok result', function () { $okResult = Ok('success value'); - + expect(function () { ResultAssertionsTestCase::assertErr($okResult); })->toThrow(ExpectationFailedException::class); @@ -38,15 +38,15 @@ class ResultAssertionsTestCase extends TestCase test('assertOk passes with Ok result', function () { $okResult = Ok('success value'); - + ResultAssertionsTestCase::assertOk($okResult); - + expect(true)->toBeTrue(); // If we get here, the assertion passed }); test('assertOk fails with Err result', function () { $errResult = Err('error message'); - + expect(function () { ResultAssertionsTestCase::assertOk($errResult); })->toThrow(ExpectationFailedException::class); @@ -60,7 +60,7 @@ class ResultAssertionsTestCase extends TestCase test('assertErr with custom message', function () { $okResult = Ok('success value'); - + try { ResultAssertionsTestCase::assertErr($okResult, 'Custom error message'); expect(false)->toBeTrue(); // Should not reach here @@ -71,7 +71,7 @@ class ResultAssertionsTestCase extends TestCase test('assertOk with custom message', function () { $errResult = Err('error message'); - + try { ResultAssertionsTestCase::assertOk($errResult, 'Custom error message'); expect(false)->toBeTrue(); // Should not reach here @@ -82,12 +82,12 @@ class ResultAssertionsTestCase extends TestCase test('isErr constraint returns IsErr instance', function () { $constraint = ResultAssertionsTestCase::isErr(); - + expect($constraint)->toBeInstanceOf(\Superscript\Monads\Result\Testing\IsErr::class); }); test('isOk constraint returns IsOk instance', function () { $constraint = ResultAssertionsTestCase::isOk(); - + expect($constraint)->toBeInstanceOf(\Superscript\Monads\Result\Testing\IsOk::class); -}); \ No newline at end of file +}); diff --git a/tests/Result/types.php b/tests/Result/types.php index 77737f4..51fca45 100644 --- a/tests/Result/types.php +++ b/tests/Result/types.php @@ -53,4 +53,4 @@ assertType('int', $x->unwrapOrElse(fn() => 2)); /** @var Result, Throwable> $x */ -assertType(Option::class . '<'.Result::class.'>', $x->transpose()); +assertType(Option::class . '<' . Result::class . '>', $x->transpose()); diff --git a/tests/php85-bootstrap.php b/tests/php85-bootstrap.php new file mode 100644 index 0000000..73c5d69 --- /dev/null +++ b/tests/php85-bootstrap.php @@ -0,0 +1,17 @@ += 80500) { + // These files contain actual pipe operator syntax (|>) + // and will cause parse errors on PHP < 8.5 + // They use .php85 extension to prevent auto-discovery by PHPUnit + require_once __DIR__ . '/Option/Php85PipeOperatorTest.php85'; + require_once __DIR__ . '/Result/Php85PipeOperatorTest.php85'; +}