Skip to content

Commit

Permalink
Feature: add Option::ify() & Option::tryIfy()
Browse files Browse the repository at this point in the history
  • Loading branch information
mathroc committed Jul 19, 2023
1 parent 630f918 commit 7c37772
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,61 @@ function tryOf(
}
}

/**
* Wrap a callable into one that return transform its result into an `Option`.
* It will be a `Some` option containing the result if it is different from `$noneValue` (default `null`).
*
* # Examples
*
* ```
* 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));
* ```
*
* @template U
* @param callable():U $callback
* @return \Closure(mixed...):Option<U>
*/
function ify(callable $callback, mixed $noneValue = null, bool $strict = true): \Closure
{
return static fn (...$args) => Option\fromValue($callback(...$args), $noneValue, $strict);
}

/**
* Wrap a callable into one that return transform its result into an `Option` like `Option\ify()` does
* but also return `Option\None` if it an exception matching $exceptionClass was thrown.
*
* # Examples
*
* ```
* 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));
* ```
*
* @template U
* @param callable():U $callback
* @return \Closure(mixed...):Option<U>
*/
function tryIfy(
callable $callback,
mixed $noneValue = null,
bool $strict = true,
string $exceptionClass = \Exception::class,
): \Closure
{
return static function (...$args) use ($callback, $noneValue, $strict, $exceptionClass): mixed {
try {
return Option\fromValue($callback(...$args), $noneValue, $strict);
} catch (\Throwable $th) {
if (\is_a($th, $exceptionClass)) {
return Option\none();
}

throw $th;
}
};
}

/**
* Converts from `Option<Option<T>>` to `Option<T>`.
*
Expand Down
72 changes: 72 additions & 0 deletions tests/Unit/Option/IfyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php declare(strict_types=1);

namespace TH\Maybe\Tests\Unit\Option;

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

final class IfyTest extends TestCase
{
use Provider\Values;

/**
* @dataProvider fromValueMatrix
* @param Option<mixed> $expected
*/
public function testIfy(Option $expected, mixed $value, mixed $noneValue, bool $strict = true): void
{
Assert::assertEquals($expected, Option\ify(static fn () => $value, $noneValue, strict: $strict)());
}

/**
* @dataProvider fromValueMatrix
* @param Option<mixed> $expected
*/
public function testTryOf(Option $expected, mixed $value, mixed $noneValue, bool $strict = true): void
{
Assert::assertEquals($expected, Option\tryIfy(static fn () => $value, $noneValue, strict: $strict)());
}

public function testOfDefaultToNull(): void
{
Assert::assertEquals(Option\none(), Option\ify(static fn () => null)());
Assert::assertEquals(Option\some(1), Option\ify(static fn () => 1)());
}

public function testTryOfDefaultToNull(): void
{
Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => null)());
Assert::assertEquals(Option\some(1), Option\tryIfy(static fn () => 1)());
}

public function testOfDefaultToStrict(): void
{
$o = (object)[];

Assert::assertEquals(Option\none(), Option\ify(static fn () => $o, (object)[], strict: false)());
Assert::assertEquals($o, Option\ify(static fn () => $o, (object)[])()->unwrap());
}

public function testTryOfDefaultToStrict(): void
{
$o = (object)[];

Assert::assertEquals(Option\none(), Option\tryIfy(static fn () => $o, (object)[], strict: false)());
Assert::assertEquals($o, Option\tryIfy(static fn () => $o, (object)[])()->unwrap());
}

public function testTryOfExeptions(): void
{
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryIfy(static fn () => new \DateTimeImmutable("none"))(), Option\none());

try {
// @phpstan-ignore-next-line
Assert::assertEquals(Option\tryIfy(static fn () => 1 / 0)(), Option\none());
Assert::fail("An exception should have been thrown");
} catch (\DivisionByZeroError) {
}
}
}

0 comments on commit 7c37772

Please sign in to comment.