diff --git a/src/Psl/Fun/passthrough.php b/src/Psl/Fun/passthrough.php new file mode 100644 index 0000000..b7d4a50 --- /dev/null +++ b/src/Psl/Fun/passthrough.php @@ -0,0 +1,18 @@ + $result; +} diff --git a/src/Psl/Fun/rethrow.php b/src/Psl/Fun/rethrow.php new file mode 100644 index 0000000..81661da --- /dev/null +++ b/src/Psl/Fun/rethrow.php @@ -0,0 +1,21 @@ +exception); + } + + /** + * The method can be used to transform a result into another result. + * Since this is a failure result wrapper, the `$on_failure` callback will be triggered. + * The callback will receive the Exception as an argument, + * so that you can use it to create a success result or rethrow the Exception. + */ + public function then(callable $on_success, callable $on_failure): ResultInterface + { + return wrap(fn () => $on_failure($this->exception)); + } } diff --git a/src/Psl/Result/ResultInterface.php b/src/Psl/Result/ResultInterface.php index f763e81..2d66b84 100644 --- a/src/Psl/Result/ResultInterface.php +++ b/src/Psl/Result/ResultInterface.php @@ -55,4 +55,38 @@ interface ResultInterface * @return bool - `true` if the operation failed; `false` otherwise */ public function isFailed(): bool; + + /** + * Unwrapping and transforming a result can be done by using the proceed method. + * The implementation will either run the `$on_success` or `$on_failure` callback. + * The callback will receive the result or Exception as an argument, + * so that you can transform it to anything you want. + * + * @template R + * + * @psalm-param callable(T): R $on_success + * @psalm-param callable(Exception): R $on_failure + * + * @psalm-return R + */ + public function proceed(callable $on_success, callable $on_failure); + + /** + * The method can be used to transform a result into another result. + * The implementation will either run the `$on_success` or `$on_failure` callback. + * The callback will receive the result value or Exception as an argument, + * so that you can transform use it to build a new result. + * + * This method is compatible with the `PromiseInterface::then()` function from `reactphp/promise`. + * You can use it in an async context as long as the package you are using is compatible with reactphp promises. + * + * @link https://github.com/reactphp/promise#promiseinterfacethen + * + * @template R + * @psalm-param callable(T): R $on_success + * @psalm-param callable(Exception): R $on_failure + * + * @psalm-return ResultInterface + */ + public function then(callable $on_success, callable $on_failure): ResultInterface; } diff --git a/src/Psl/Result/Success.php b/src/Psl/Result/Success.php index 9626b4e..929a841 100644 --- a/src/Psl/Result/Success.php +++ b/src/Psl/Result/Success.php @@ -73,4 +73,24 @@ final class Success implements ResultInterface { return false; } + + /** + * Unwrapping and transforming a result can be done by using the proceed method. + * Since this is a success result wrapper, the `$on_success` callback will be triggered. + * The callback will receive the result value as an argument, so that you can transform it to anything you want. + */ + public function proceed(callable $on_success, callable $on_failure) + { + return $on_success($this->value); + } + + /** + * The method can be used to transform a result into another result. + * Since this is a success result wrapper, the `$on_success` callback will be triggered. + * The callback will receive the result value as an argument, so that you can use it to create a new result. + */ + public function then(callable $on_success, callable $on_failure): ResultInterface + { + return wrap(fn () => $on_success($this->value)); + } } diff --git a/tests/Psl/Fun/PassthroughTest.php b/tests/Psl/Fun/PassthroughTest.php new file mode 100644 index 0000000..e4be9e3 --- /dev/null +++ b/tests/Psl/Fun/PassthroughTest.php @@ -0,0 +1,19 @@ +expectExceptionObject($exception); + $rethrow($exception); + } +} diff --git a/tests/Psl/Result/FailureTest.php b/tests/Psl/Result/FailureTest.php index 4a67578..d6a1c19 100644 --- a/tests/Psl/Result/FailureTest.php +++ b/tests/Psl/Result/FailureTest.php @@ -4,26 +4,29 @@ declare(strict_types=1); namespace Psl\Tests\Result; +use Exception; use PHPUnit\Framework\TestCase; +use Psl\Fun; use Psl\Result\Failure; +use stdClass; class FailureTest extends TestCase { public function testIsSucceeded(): void { - $wrapper = new Failure(new \Exception('foo')); + $wrapper = new Failure(new Exception('foo')); self::assertFalse($wrapper->isSucceeded()); } public function testIsFailed(): void { - $wrapper = new Failure(new \Exception('foo')); + $wrapper = new Failure(new Exception('foo')); self::assertTrue($wrapper->isFailed()); } public function testGetResult(): void { - $exception = new \Exception('bar'); + $exception = new Exception('bar'); $wrapper = new Failure($exception); $this->expectExceptionObject($exception); @@ -32,9 +35,51 @@ class FailureTest extends TestCase public function testGetException(): void { - $exception = new \Exception('bar'); + $exception = new Exception('bar'); $wrapper = new Failure($exception); $e = $wrapper->getException(); self::assertSame($exception, $e); } + + public function testProceed(): void + { + $exception = new Exception('bar'); + $wrapper = new Failure($exception); + $actual = $wrapper->proceed( + static fn (string $result): int => 200, + static fn (Exception $exception): int => 404 + ); + + self::assertSame(404, $actual); + } + + public function testThenToSuccess(): void + { + $exception = new Exception('bar'); + $wrapper = new Failure($exception); + $actual = $wrapper->then( + static function () { + throw new \Exception('Dont call us, we\'ll call you!'); + }, + static fn (Exception $exception): string => $exception->getMessage() + ); + + self::assertTrue($actual->isSucceeded()); + self::assertSame($actual->getResult(), 'bar'); + } + + public function testThenToFailure(): void + { + $exception = new Exception('bar'); + $wrapper = new Failure($exception); + $actual = $wrapper->then( + static function () { + throw new \Exception('Dont call us, we\'ll call you!'); + }, + Fun\rethrow() + ); + + self::assertFalse($actual->isSucceeded()); + self::assertSame($actual->getException(), $exception); + } } diff --git a/tests/Psl/Result/SuccessTest.php b/tests/Psl/Result/SuccessTest.php index b96ab3f..ffe05f9 100644 --- a/tests/Psl/Result/SuccessTest.php +++ b/tests/Psl/Result/SuccessTest.php @@ -4,9 +4,12 @@ declare(strict_types=1); namespace Psl\Tests\Result; +use Exception; use PHPUnit\Framework\TestCase; +use Psl\Fun; use Psl\Result\Success; use Psl\Exception\InvariantViolationException; +use stdClass; class SuccessTest extends TestCase { @@ -37,4 +40,43 @@ class SuccessTest extends TestCase $wrapper->getException(); } + + public function testProceed(): void + { + $wrapper = new Success('hello'); + $actual = $wrapper->proceed( + static fn (string $result): int => 200, + static fn (Exception $exception): int => 404 + ); + + self::assertSame(200, $actual); + } + + public function testThenToSuccess(): void + { + $wrapper = new Success('hello'); + $actual = $wrapper->then( + Fun\passthrough(), + Fun\rethrow() + ); + + self::assertNotSame($wrapper, $actual); + self::assertTrue($actual->isSucceeded()); + self::assertSame('hello', $actual->getResult()); + } + + public function testThenToFailure(): void + { + $exception = new Exception('bar'); + $wrapper = new Success('hello'); + $actual = $wrapper->then( + static function () use ($exception) { + throw $exception; + }, + Fun\rethrow() + ); + + self::assertFalse($actual->isSucceeded()); + self::assertSame($actual->getException(), $exception); + } }