From 0c865b95cd477107f0a190fe24dfda844587b9ae Mon Sep 17 00:00:00 2001 From: Samuel Maudo Date: Mon, 16 Oct 2023 23:32:07 +0200 Subject: [PATCH] Add factory class --- docs/.vitepress/config.ts | 6 +- docs/reference/index.md | 5 + docs/reference/result.md | 49 ++++++++++ psalm.xml | 5 + src/Result.php | 99 +++++++++++++++++++ tests/ResultTest.php | 193 ++++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 36 +++++-- 7 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 docs/reference/result.md create mode 100644 src/Result.php create mode 100644 tests/ResultTest.php diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index da592a6..42bb99d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -36,7 +36,8 @@ function nav() { activeMatch: '/reference/', items: [ { text: 'Ok', link: '/reference/ok' }, - { text: 'Error', link: '/reference/error' } + { text: 'Error', link: '/reference/error' }, + { text: 'Result', link: '/reference/result' } ] } ] @@ -55,7 +56,8 @@ function sidebarReference() { link: '/reference/', items: [ { text: 'Ok', link: '/reference/ok' }, - { text: 'Error', link: '/reference/error' } + { text: 'Error', link: '/reference/error' }, + { text: 'Result', link: '/reference/result' } ] } ] diff --git a/docs/reference/index.md b/docs/reference/index.md index 152f1f3..5ecfb3e 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -7,3 +7,8 @@ Results - [`Ok`](ok) contains the success value. - [`Error`](error) contains the error exception. + +Factory +------- + +- [`Result`](result) makes new results. diff --git a/docs/reference/result.md b/docs/reference/result.md new file mode 100644 index 0000000..6818f4f --- /dev/null +++ b/docs/reference/result.md @@ -0,0 +1,49 @@ + +# Result + + +Factory class to make new `Ok` and `Error` instances. + +This class cannot be instantiated. + + +## Static Methods + + +### of + +```php +public static function of(mixed $value): Ok|Error; +``` + +Makes an `Ok` with the given `value`. + +**Note:** If `value` is a closure, this method will call it and +use the returned value to make the result, returning an `Error` +if any exception is thrown. + + +### fromFalsable + +```php +public static function fromFalsable(mixed $value): Ok|Error; +``` + +Makes an empty `Error` if the value is `false`. Otherwise, +makes an `Ok` with the given `value`. + +**Note:** If `value` is a closure, this method will call it and +use the returned value to make the result. + + +### fromNullable + +```php +public static function fromNullable(mixed $value): Ok|Error; +``` + +Makes an empty `Error` if the value is `null`. Otherwise, +makes an `Ok` with the given `value`. + +**Note:** If `value` is a closure, this method will call it and +use the returned value to make the result. diff --git a/psalm.xml b/psalm.xml index 5ecc18d..342da58 100644 --- a/psalm.xml +++ b/psalm.xml @@ -19,6 +19,11 @@ + + + + + diff --git a/src/Result.php b/src/Result.php new file mode 100644 index 0000000..9a7c77b --- /dev/null +++ b/src/Result.php @@ -0,0 +1,99 @@ +|Error + * + * @psalm-suppress MixedAssignment + */ + public static function of(mixed $value): Ok|Error + { + if ($value instanceof Closure) { + try { + $value = $value(); + } catch (Throwable $e) { + return Error::withException($e); + } + } + + return Ok::withValue($value); + } + + /** + * Makes an empty `Error` if the value is `null`. Otherwise, makes + * an `Ok` with the given `value`. + * + * **Note:** If `value` is a closure, this method will call it and + * use the returned value to make the result. + * + * @template U + * + * @param (U|null)|Closure():(U|null) $value + * + * @return Ok|Error + * + * @psalm-suppress MixedAssignment + */ + public static function fromNullable(mixed $value): Ok|Error + { + if ($value instanceof Closure) { + $value = $value(); + } + + return ($value === null) + ? Error::empty() + : Ok::withValue($value); + } + + /** + * Makes an empty `Error` if the value is `false`. Otherwise, + * makes an `Ok` with the given `value`. + * + * **Note:** If `value` is a closure, this method will call it and + * use the returned value to make the result. + * + * @template U + * + * @param (U|false)|Closure():(U|false) $value + * + * @return Ok|Error + * + * @psalm-suppress MixedAssignment + */ + public static function fromFalsable(mixed $value): Ok|Error + { + if ($value instanceof Closure) { + $value = $value(); + } + + return ($value === false) + ? Error::empty() + : Ok::withValue($value); + } +} diff --git a/tests/ResultTest.php b/tests/ResultTest.php new file mode 100644 index 0000000..692815f --- /dev/null +++ b/tests/ResultTest.php @@ -0,0 +1,193 @@ + new Result() // @phpstan-ignore-line + ); + } + + public function testOfValue(): void + { + $result = Result::of(null); + self::assertInstanceOf(Ok::class, $result); + self::assertNull($result->value()); + + $result = Result::of(false); + self::assertInstanceOf(Ok::class, $result); + self::assertFalse($result->value()); + + $value = $this->random()->randomNumber(); + $result = Result::of($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::of($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::of($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + } + + public function testOfClosure(): void + { + $result = Result::of(fn() => null); + self::assertInstanceOf(Ok::class, $result); + self::assertNull($result->value()); + + $result = Result::of(fn() => false); + self::assertInstanceOf(Ok::class, $result); + self::assertFalse($result->value()); + + $value = $this->random()->randomNumber(); + $result = Result::of(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::of(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::of(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $exception = new RuntimeException(); + $result = Result::of(fn() => throw $exception); + self::assertInstanceOf(Error::class, $result); + self::assertSame($exception, $result->exception()); + } + + public function testFromNullableValue(): void + { + $result = Result::fromNullable(null); + self::assertInstanceOf(Error::class, $result); + + $result = Result::fromNullable(false); + self::assertInstanceOf(Ok::class, $result); + self::assertFalse($result->value()); + + $value = $this->random()->randomNumber(); + $result = Result::fromNullable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::fromNullable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::fromNullable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + } + + public function testFromNullableClosure(): void + { + $result = Result::fromNullable(fn() => null); + self::assertInstanceOf(Error::class, $result); + + $result = Result::fromNullable(fn() => false); + self::assertInstanceOf(Ok::class, $result); + self::assertFalse($result->value()); + + $value = $this->random()->randomNumber(); + $result = Result::fromNullable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::fromNullable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::fromNullable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $exception = new RuntimeException(); + self::assertException( + $exception, + fn() => Result::fromNullable(fn() => throw $exception) + ); + } + + public function testFromFalsableValue(): void + { + $result = Result::fromFalsable(null); + self::assertInstanceOf(Ok::class, $result); + self::assertNull($result->value()); + + $result = Result::fromFalsable(false); + self::assertInstanceOf(Error::class, $result); + + $value = $this->random()->randomNumber(); + $result = Result::fromFalsable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::fromFalsable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::fromFalsable($value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + } + + public function testFromFalsableClosure(): void + { + $result = Result::fromFalsable(fn() => null); + self::assertInstanceOf(Ok::class, $result); + self::assertNull($result->value()); + + $result = Result::fromFalsable(fn() => false); + self::assertInstanceOf(Error::class, $result); + + $value = $this->random()->randomNumber(); + $result = Result::fromFalsable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->randomFloat(); + $result = Result::fromFalsable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $value = $this->random()->word(); + $result = Result::fromFalsable(fn() => $value); + self::assertInstanceOf(Ok::class, $result); + self::assertSame($value, $result->value()); + + $exception = new RuntimeException(); + self::assertException( + $exception, + fn() => Result::fromFalsable(fn() => throw $exception) + ); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index f8d78b4..248e785 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,6 +7,8 @@ use Faker\Factory as FakerFactory; use Faker\Generator as FakerGenerator; use PHPUnit\Framework\Constraint\Exception as ExceptionConstraint; +use PHPUnit\Framework\Constraint\ExceptionCode; +use PHPUnit\Framework\Constraint\ExceptionMessageIsOrContains; use PHPUnit\Framework\Constraint\IsIdentical; use PHPUnit\Framework\TestCase as PHPUnitTestCase; use Throwable; @@ -16,27 +18,41 @@ abstract class TestCase extends PHPUnitTestCase private FakerGenerator|null $random = null; /** - * @param class-string $expectedException + * @param Throwable|class-string $expectedException * * @psalm-suppress InternalClass * @psalm-suppress InternalMethod */ public static function assertException( - string $expectedException, + Throwable|string $expectedException, callable $callback ): void { + $exception = null; + try { $callback(); - $exception = null; } catch (Throwable $exception) { } - /** @psalm-suppress PossiblyUndefinedVariable */ - static::assertThat( - $exception, - new ExceptionConstraint( - $expectedException - ) - ); + + if (is_string($expectedException)) { + static::assertThat( + $exception, + new ExceptionConstraint($expectedException) + ); + } else { + static::assertThat( + $exception, + new ExceptionConstraint($expectedException::class) + ); + static::assertThat( + $exception?->getMessage(), + new ExceptionMessageIsOrContains($expectedException->getMessage()) + ); + static::assertThat( + $exception?->getCode(), + new ExceptionCode($expectedException->getCode()) + ); + } } public static function assertExceptionMessage(