From abc577325ab441b76d57687e583f593a15242a5f Mon Sep 17 00:00:00 2001 From: azjezz Date: Sun, 1 Mar 2020 11:49:22 +0100 Subject: [PATCH] [Asio] introduce ResultOrException wrapper --- phpunit.xml.dist | 3 + psalm.xml | 35 ++++++++++ src/Psl/Asio/IResultOrExceptionWrapper.php | 57 ++++++++++++++++ src/Psl/Asio/WrappedException.php | 69 ++++++++++++++++++++ src/Psl/Asio/WrappedResult.php | 75 ++++++++++++++++++++++ src/Psl/Asio/wrap.php | 25 ++++++++ src/Psl/Str/format.php | 2 +- src/Psl/internal.php | 12 ++++ src/Psl/invariant.php | 8 ++- src/bootstrap.php | 1 + tests/Psl/Asio/WrapTest.php | 42 ++++++++++++ tests/Psl/Asio/WrappedExceptionTest.php | 40 ++++++++++++ tests/Psl/Asio/WrappedResultTest.php | 39 +++++++++++ 13 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 src/Psl/Asio/IResultOrExceptionWrapper.php create mode 100644 src/Psl/Asio/WrappedException.php create mode 100644 src/Psl/Asio/WrappedResult.php create mode 100644 src/Psl/Asio/wrap.php create mode 100644 tests/Psl/Asio/WrapTest.php create mode 100644 tests/Psl/Asio/WrappedExceptionTest.php create mode 100644 tests/Psl/Asio/WrappedResultTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0a58a81..6ececf9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,6 +16,9 @@ tests/Psl/Arr + + tests/Psl/Asio + tests/Psl/Collection diff --git a/psalm.xml b/psalm.xml index c2dabe5..a02880d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -13,4 +13,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Psl/Asio/IResultOrExceptionWrapper.php b/src/Psl/Asio/IResultOrExceptionWrapper.php new file mode 100644 index 0000000..4ee7223 --- /dev/null +++ b/src/Psl/Asio/IResultOrExceptionWrapper.php @@ -0,0 +1,57 @@ +` by calling `wrap()`, passing in the `callable(): T`, + * and a `WrappedResult` or `WrappedException` is returned. + * + * @template T + */ +interface IResultOrExceptionWrapper +{ + /** + * Return the result of the operation, or throw underlying exception. + * + * - if the operation succeeded: return its result. + * - if the operation failed: throw the exception inciting failure. + * + * @psalm-return T - The result of the operation upon success + */ + public function getResult(); + + /** + * Return the underlying exception, or fail with a invariant violation exception exception. + * + * - if the operation succeeded: fails with a invariant violation exception. + * - if the operation failed: returns the exception indicating failure. + * + * @throws Psl\Exception\InvariantViolationException - When the operation succeeded + */ + public function getException(): Exception; + + /** + * Indicates whether the operation associated with this wrapper existed normally. + * + * if `isSucceeded()` returns `true`, `isFailed()` returns false. + * + * @return bool - `true` if the operation succeeded; `false` otherwise + */ + public function isSucceeded(): bool; + + /** + * Indicates whether the operation associated with this wrapper exited abnormally via an exception of some sort. + * + * if `isFailed()` returns `true`, `isSucceeded()` returns false. + * + * @return bool - `true` if the operation failed; `false` otherwise + */ + public function isFailed(): bool; +} diff --git a/src/Psl/Asio/WrappedException.php b/src/Psl/Asio/WrappedException.php new file mode 100644 index 0000000..1b9559c --- /dev/null +++ b/src/Psl/Asio/WrappedException.php @@ -0,0 +1,69 @@ + + */ +final class WrappedException implements IResultOrExceptionWrapper +{ + /** + * @psalm-var Te + */ + private Exception $exception; + + /** + * @psalm-param Te $exception + */ + public function __construct(Exception $exception) + { + $this->exception = $exception; + } + + /** + * Since this is a failed result wrapper, this always throws the exception thrown during the operation. + * + * @throws Exception + */ + public function getResult(): void + { + throw $this->exception; + } + + /** + * Since this is a failed result wrapper, this always returns the exception thrown during the operation. + * + * @psalm-return Te - The exception thrown during the operation. + */ + public function getException(): Exception + { + return $this->exception; + } + + /** + * Since this is a failed result wrapper, this always returns `false`. + */ + public function isSucceeded(): bool + { + return false; + } + + /** + * Since this is a failed result wrapper, this always returns `true`. + */ + public function isFailed(): bool + { + return true; + } +} diff --git a/src/Psl/Asio/WrappedResult.php b/src/Psl/Asio/WrappedResult.php new file mode 100644 index 0000000..cca41e8 --- /dev/null +++ b/src/Psl/Asio/WrappedResult.php @@ -0,0 +1,75 @@ + + */ +final class WrappedResult implements IResultOrExceptionWrapper +{ + /** + * @psalm-var T + */ + private $value; + + /** + * @psalm-param T $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * Since this is a successful result wrapper, this always returns the actual result of the operation. + * + * @psalm-return T + */ + public function getResult() + { + return $this->value; + } + + /** + * Since this is a successful result wrapper, this always throws a + * `Psl\Exception\InvariantViolationException` saying that there was no exception thrown from the operation. + * + * @throws Psl\Exception\InvariantViolationException + * + * @psalm-return no-return + */ + public function getException(): Exception + { + Psl\invariant_violation('No exception thrown from the operation.'); + } + + /** + * Since this is a successful result wrapper, this always returns `true`. + * + * @return true + */ + public function isSucceeded(): bool + { + return true; + } + + /** + * Since this is a successful result wrapper, this always returns `false`. + * + * @return false + */ + public function isFailed(): bool + { + return false; + } +} diff --git a/src/Psl/Asio/wrap.php b/src/Psl/Asio/wrap.php new file mode 100644 index 0000000..5d35dc7 --- /dev/null +++ b/src/Psl/Asio/wrap.php @@ -0,0 +1,25 @@ + + */ +function wrap(callable $fun): IResultOrExceptionWrapper +{ + try { + $result = $fun(); + return new WrappedResult($result); + } catch (\Exception $e) { + return new WrappedException($e); + } +} diff --git a/src/Psl/Str/format.php b/src/Psl/Str/format.php index 5732a8a..893333e 100644 --- a/src/Psl/Str/format.php +++ b/src/Psl/Str/format.php @@ -26,7 +26,7 @@ namespace Psl\Str; * Str\format('%s is %d character(s) long.', 'س', Str\length('س')); * => Str('س is 1 character(s) long.') * - * @psalm-param int|float|string|bool ...$args + * @psalm-param int|float|string ...$args * * @return string a string produced according to the formatting string * format diff --git a/src/Psl/internal.php b/src/Psl/internal.php index cecb858..95381e0 100644 --- a/src/Psl/internal.php +++ b/src/Psl/internal.php @@ -60,3 +60,15 @@ function boolean($val): bool { return (bool) $val; } + +/** + * @param mixed $value + * + * @return string + * + * @codeCoverageIgnore + */ +function type($value): string +{ + return \is_object($value) ? \get_class($value) : \gettype($value); +} diff --git a/src/Psl/invariant.php b/src/Psl/invariant.php index 82e20e8..3a7543c 100644 --- a/src/Psl/invariant.php +++ b/src/Psl/invariant.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Psl; /** - * @psalm-param int|float|string|bool ...$args + * @psalm-param int|float|string ...$args * * @psalm-assert true $fact */ @@ -17,9 +17,11 @@ function invariant(bool $fact, string $message, ...$args): void } /** - * @psalm-param int|float|string|bool ...$args + * @psalm-param int|float|string ...$args * - * @pslam-return no-return + * @psalm-return no-return + * + * @throws Exception\InvariantViolationException */ function invariant_violation(string $format, ...$args): void { diff --git a/src/bootstrap.php b/src/bootstrap.php index 82cddf2..c45d055 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -11,6 +11,7 @@ function __bootstrap(): void { static $booted = false; static $files = [ + 'Psl/Asio/wrap.php', '/Psl/Random/string.php', '/Psl/Random/int.php', '/Psl/Random/float.php', diff --git a/tests/Psl/Asio/WrapTest.php b/tests/Psl/Asio/WrapTest.php new file mode 100644 index 0000000..3f46ab9 --- /dev/null +++ b/tests/Psl/Asio/WrapTest.php @@ -0,0 +1,42 @@ +assertFalse($wrapper->isSucceeded()); + $this->assertTrue($wrapper->isFailed()); + $this->assertSame($exception, $wrapper->getException()); + + $this->expectExceptionObject($exception); + + $wrapper->getResult(); + } + + public function testWrapResult(): void + { + $wrapper = Asio\wrap(static function(): string { + return 'foo'; + }); + $this->assertTrue($wrapper->isSucceeded()); + $this->assertFalse($wrapper->isFailed()); + $this->assertSame('foo', $wrapper->getResult()); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('No exception thrown'); + + $wrapper->getException(); + } +} diff --git a/tests/Psl/Asio/WrappedExceptionTest.php b/tests/Psl/Asio/WrappedExceptionTest.php new file mode 100644 index 0000000..1220afb --- /dev/null +++ b/tests/Psl/Asio/WrappedExceptionTest.php @@ -0,0 +1,40 @@ +assertFalse($wrapper->isSucceeded()); + } + + public function testIsFailed(): void + { + $wrapper = new WrappedException(new \Exception('foo')); + $this->assertTrue($wrapper->isFailed()); + } + + public function testGetResult(): void + { + $exception = new \Exception('bar'); + $wrapper = new WrappedException($exception); + + $this->expectExceptionObject($exception); + $wrapper->getResult(); + } + + public function testGetException(): void + { + $exception = new \Exception('bar'); + $wrapper = new WrappedException($exception); + $e = $wrapper->getException(); + $this->assertSame($exception, $e); + } +} diff --git a/tests/Psl/Asio/WrappedResultTest.php b/tests/Psl/Asio/WrappedResultTest.php new file mode 100644 index 0000000..7f007d9 --- /dev/null +++ b/tests/Psl/Asio/WrappedResultTest.php @@ -0,0 +1,39 @@ +assertTrue($wrapper->isSucceeded()); + } + + public function testIsFailed(): void + { + $wrapper = new WrappedResult('hello'); + $this->assertFalse($wrapper->isFailed()); + } + + public function testGetResult(): void + { + $wrapper = new WrappedResult('hello'); + $this->assertSame('hello', $wrapper->getResult()); + } + + public function testGetException(): void + { + $wrapper = new WrappedResult('hello'); + + $this->expectException(InvariantViolationException::class); + $this->expectExceptionMessage('No exception thrown'); + + $wrapper->getException(); + } +}