Skip to content

Commit

Permalink
Feature: add Result::trap()
Browse files Browse the repository at this point in the history
  • Loading branch information
mathroc committed Jul 19, 2023
1 parent a67cf3e commit 46e702e
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 17 deletions.
66 changes: 60 additions & 6 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,15 @@ function fromValue(mixed $value, mixed $noneValue = null, bool $strict = true):
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Option\of(fn() => "fruits"), Option\some("fruits"));
* ```
*
* Convertion of `null` to `Option\None`:
*
* ```
* self::assertEq(Option\of(fn() => null), Option\none());
* ```
*
Expand All @@ -81,9 +88,29 @@ function of(callable $callback, mixed $noneValue = null, bool $strict = true): O
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Option\tryOf(fn () => strtolower("FRUITS")), Option\some("fruits"));
* ```
*
* Convertion of `null` to `Option\None`:
*
* ```
* self::assertEq(Option\tryOf(fn() => null), Option\none());
* ```
*
* Checked Exception:
*
* ```
* self::assertEq(Option\tryOf(fn () => new \DateTimeImmutable("nope")), Option\none());
* ```
*
* Unchecked Exception:
*
* ```
* self::assertEq(Option\tryOf(fn() => new \DateTimeImmutable("nope")), Option\none());
* Option\tryOf(fn() => 1 / 0); // @throws DivisionByZeroError Division by zero
* self::assertEq(Option\tryOf(fn () => 1 / 0), Option\none());
* // @throws DivisionByZeroError Division by zero
* ```
*
* @template U
Expand Down Expand Up @@ -114,9 +141,16 @@ function tryOf(
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Option\ify(strtolower(...))("FRUITS"), Option\some("fruits"));
* ```
*
* Convertion of `null` to `Option\None`:
*
* ```
* self::assertEq(Option\ify(strtotime(...), noneValue: false)("nope"), Option\none());
* self::assertEq(Option\ify(strtotime(...))("2015-09-21 UTC at midnight"), Option\some(1_442_793_600));
* self::assertEq(Option\ify(fn() => null)(), Option\none());
* ```
*
* @template U
Expand All @@ -134,9 +168,29 @@ function ify(callable $callback, mixed $noneValue = null, bool $strict = true):
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Option\tryIfy(strtolower(...))("FRUITS"), Option\some("fruits"));
* ```
*
* Convertion of `null` to `Option\None`:
*
* ```
* self::assertEq(Option\tryIfy(fn() => null)(), Option\none());
* ```
*
* Checked Exception:
*
* ```
* self::assertEq(Option\tryIfy(fn () => new \DateTimeImmutable("nope"))(), Option\none());
* ```
*
* Unchecked Exception:
*
* ```
* self::assertEq(Option\ify(strtotime(...), noneValue: false)("nope"), Option\none());
* self::assertEq(Option\ify(strtotime(...))("2015-09-21 UTC at midnight"), Option\some(1_442_793_600));
* self::assertEq(Option\tryIfy(fn () => 1 / 0)(), Option\none());
* // @throws DivisionByZeroError Division by zero
* ```
*
* @template U
Expand Down
72 changes: 67 additions & 5 deletions src/functions/result.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,35 @@
use TH\Maybe\Result;
use TH\Maybe\Tests\Helpers\IgnoreUnusedResults;

/**
* Return a `Result\Ok` Result containing `$value`.
*
* ```
* $x = Result\ok(3);
* self::assertTrue($x->isOk());
* self::assertSame(3, $x->unwrap());
* ```
*
* @template U
* @param U $value
* @return Result\Ok<U>
*/
function ok(mixed $value): Result\Ok
{
return new Result\Ok($value);
}
/**
* Return a `Result\Err` result.
*
* # Examples
*
* ```
* $x = Result\err("nope");
* self::assertTrue($x->isErr());
* self::assertSame("nope", $x->unwrapErr());
* $x->unwrap(); // @throws RuntimeException Unwrapping `Err`: s:4:"nope";
* ```
*
* @template F
* @param F $value
* @return Result\Err<F>
Expand All @@ -20,15 +46,51 @@ function err(mixed $value): Result\Err
}

/**
* Return a `Result\Ok` Result containing `$value`.
* Execute a callable and transform the result into an `Result`.
* It will be a `Result\Ok` containing the result or, if it threw an exception
* matching $exceptionClass, a `Result\Err` containing the exception.
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Result\ok(3), Result\trap(fn () => 3));
* ```
*
* Checked exception:
*
* ```
* $x = Result\trap(fn () => new \DateTimeImmutable("2020-30-30 UTC"));
* self::assertTrue($x->isErr());
* $x->unwrap();
* // @throws Exception Failed to parse time string (2020-30-30 UTC) at position 6 (0): Unexpected character
* ```
*
* Unchecked exception:
*
* ```
* Result\trap(fn () => 1/0);
* // @throws DivisionByZeroError Division by zero
* ```
*
* @template U
* @param U $value
* @return Result\Ok<U>
* @param callable():U $callback
* @return Result<U,\Throwable>
* @throws \Throwable
*/
function ok(mixed $value): Result\Ok
#[ExamplesSetup(IgnoreUnusedResults::class)]
function trap(callable $callback, string $exceptionClass = \Exception::class): Result
{
return new Result\Ok($value);
try {
return Result\ok($callback());
} catch (\Throwable $th) {
if (\is_a($th, $exceptionClass)) {
return Result\err($th);
}

throw $th;
}
}

/**
Expand Down
2 changes: 0 additions & 2 deletions tests/Provider/Values.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace TH\Maybe\Tests\Provider;

use TH\Maybe\Option;

trait Values
{
/**
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Option/IfyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ public function testTryOfDefaultToStrict(): void
public function testTryOfExeptions(): void
{
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryIfy(static fn () => new \DateTimeImmutable("none"))(), Option\none());
Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => new \DateTimeImmutable("nope"))());

try {
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryIfy(static fn () => 1 / 0)(), Option\none());
Option\tryIfy(static fn () => 1 / 0)();
Assert::fail("An exception should have been thrown");
} catch (\DivisionByZeroError) {
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/Option/OfTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ public function testTryOfDefaultToStrict(): void
public function testTryOfExeptions(): void
{
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryOf(static fn () => new \DateTimeImmutable("none")), Option\none());
Assert::assertEquals(Option\none(), Option\tryOf(static fn () => new \DateTimeImmutable("nope")));

try {
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryOf(static fn () => 1 / 0), Option\none());
Option\tryOf(static fn () => 1 / 0);
Assert::fail("An exception should have been thrown");
} catch (\DivisionByZeroError) {
}
Expand Down
48 changes: 48 additions & 0 deletions tests/Unit/Result/TrapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);

namespace TH\Maybe\Tests\Unit\Result;

use PHPUnit\Framework\TestCase;
use TH\Maybe\Result;
use TH\Maybe\Tests\Assert;
use TH\Maybe\Tests\Provider;

final class TrapTest extends TestCase
{
use Provider\Values;

/**
* @dataProvider values
*/
public function testTrapOk(mixed $value): void
{
$callback = static fn () => $value;

Assert::assertEquals($value, Result\trap($callback)->unwrap());
}

public function testTrapCheckedException(): void
{
Assert::assertEquals(
new \Exception(
"Failed to parse time string (nope) at position 0 (n): The timezone could not be found in the database",
),
// @phpstan-ignore-next-line
Result\trap(static fn () => new \DateTimeImmutable("nope"))->unwrapErr(),
);
}

public function testTrapUncheckedException(): void
{
try {
// @phpstan-ignore-next-line
Result\trap(static fn () => 1 / 0);
Assert::fail("An exception should have been thrown");
} catch (\DivisionByZeroError $ex) {
Assert::assertEquals(
"Division by zero",
$ex->getMessage(),
);
}
}
}

0 comments on commit 46e702e

Please sign in to comment.