Skip to content

Commit

Permalink
Revert "Revert "Feature: add Option::ify() & Option::tryIfy()""
Browse files Browse the repository at this point in the history
This reverts commit 95dc3bf.
  • Loading branch information
mathroc committed Sep 26, 2024
1 parent e0d930b commit 8a7d3c3
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 2 deletions.
84 changes: 82 additions & 2 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,7 @@ function of(callable $callback, mixed $noneValue = null, bool $strict = true): O
* ```
*
* @template U
* @template E of \Throwable
* @param callable():U $callback
* @param class-string<E> $exceptionClass
* @return Option<U>
* @throws \Throwable
*/
Expand All @@ -137,6 +135,88 @@ function tryOf(
}
}

/**
* Wrap a callable into one that transforms its result into an `Option`.
* It will be a `Some` option containing the result if it is different from `$noneValue` (default `null`).
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Option\ify(strtolower(...))("FRUITS"), Option\some("fruits"));
* ```
*
* Convertion of `null` to `Option\None`:
*
* ```
* self::assertEq(Option\ify(fn() => null)(), Option\none());
* ```
*
* @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 transforms its result into an `Option` like `Option\ify()` does
* but also return `Option\None` if it an exception matching $exceptionClass was thrown.
*
* # 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\tryIfy(fn () => 1 / 0)(), Option\none());
* // @throws DivisionByZeroError Division by zero
* ```
*
* @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\Options;

/**
* @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\none(), Option\tryIfy(static fn () => new \DateTimeImmutable("nope"))());

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

0 comments on commit 8a7d3c3

Please sign in to comment.