Skip to content

Commit

Permalink
Feature: add Option::of() & Option::tryOf()
Browse files Browse the repository at this point in the history
  • Loading branch information
mathroc committed Jul 19, 2023
1 parent e7fed1a commit 630f918
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 27 deletions.
2 changes: 2 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<exclude name="SlevomatCodingStandard.Commenting.RequireOneLineDocComment" />
<exclude name="SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment" />
<exclude name="SlevomatCodingStandard.ControlStructures.NewWithoutParentheses" />
<exclude name="SlevomatCodingStandard.Exceptions.DisallowNonCapturingCatch" />
<exclude name="SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly" />
<exclude name="SlevomatCodingStandard.Files.TypeNameMatchesFileName" />
<exclude name="SlevomatCodingStandard.Functions.DisallowArrowFunction" />
<exclude name="SlevomatCodingStandard.Functions.DisallowEmptyFunction" />
Expand Down
53 changes: 53 additions & 0 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,59 @@ function fromValue(mixed $value, mixed $noneValue = null, bool $strict = true):
: Option\some($value);
}

/**
* Execute a callable and transform the 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\of(fn() => "fruits"), Option\some("fruits"));
* self::assertEq(Option\of(fn() => null), Option\none());
* ```
*
* @template U
* @param callable():U $callback
* @return Option<U>
*/
function of(callable $callback, mixed $noneValue = null, bool $strict = true): Option
{
return Option\fromValue($callback(), $noneValue, $strict);
}

/**
* Execute a callable and transform the result into an `Option` as `Option\of()` does
* but also return `Option\None` if it an exception matching $exceptionClass was thrown.
*
* # Examples
*
* ```
* self::assertEq(Option\tryOf(fn() => new \DateTimeImmutable("nope")), Option\none());
* Option\tryOf(fn() => 1 / 0); // @throws DivisionByZeroError Division by zero
* ```
*
* @template U
* @param callable():U $callback
* @return Option<U>
* @throws \Throwable
*/
function tryOf(
callable $callback,
mixed $noneValue = null,
bool $strict = true,
string $exceptionClass = \Exception::class,
): Option {
try {
return Option\of($callback, $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
29 changes: 29 additions & 0 deletions tests/Provider/Values.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace TH\Maybe\Tests\Provider;

use TH\Maybe\Option;

trait Values
{
/**
Expand All @@ -15,4 +17,31 @@ public function values(): iterable
yield "object" => [(object)[]];
yield "datetime" => [new \DateTimeImmutable()];
}

/**
* @return iterable<array{Option<mixed>, mixed, mixed, 3?:bool}>
*/
public function fromValueMatrix(): iterable
{
$o = (object)[];

yield [Option\none(), null, null];
yield [Option\some(null), null, 0];

yield [Option\none(), 0, 0];
yield [Option\some(0), 0, 1];
yield [Option\none(), 1, 1];
yield [Option\some(1), 1, 0];
yield [Option\some(1), 1, '1'];
yield [Option\none(), 1, '1', false];
yield [Option\none(), 1, true, false];
yield [Option\some(1), 1, true];

yield [Option\none(), [], []];
yield [Option\some([1]), [1], [2]];

yield [Option\none(), $o, $o];
yield [Option\some($o), $o, (object)[]];
yield [Option\none(), $o, (object)[], false];
}
}
27 changes: 0 additions & 27 deletions tests/Unit/Option/FromValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,6 @@ public function testFromValue(Option $expected, mixed $value, mixed $noneValue,
Assert::assertEquals($expected, Option\fromValue($value, $noneValue, strict: $strict));
}

/**
* @return iterable<array{Option<mixed>, mixed, mixed, 3?:bool}>
*/
public function fromValueMatrix(): iterable
{
$o = (object)[];

yield [Option\none(), null, null];
yield [Option\some(null), null, 0];

yield [Option\none(), 0, 0];
yield [Option\some(0), 0, 1];
yield [Option\none(), 1, 1];
yield [Option\some(1), 1, 0];
yield [Option\some(1), 1, '1'];
yield [Option\none(), 1, '1', false];
yield [Option\none(), 1, true, false];
yield [Option\some(1), 1, true];

yield [Option\none(), [], []];
yield [Option\some([1]), [1], [2]];

yield [Option\none(), $o, $o];
yield [Option\some($o), $o, (object)[]];
yield [Option\none(), $o, (object)[], false];
}

public function testFromValueDefaultToNull(): void
{
Assert::assertEquals(Option\none(), Option\fromValue(null));
Expand Down
72 changes: 72 additions & 0 deletions tests/Unit/Option/OfTest.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 OfTest extends TestCase
{
use Provider\Values;

/**
* @dataProvider fromValueMatrix
* @param Option<mixed> $expected
*/
public function testOf(Option $expected, mixed $value, mixed $noneValue, bool $strict = true): void
{
Assert::assertEquals($expected, Option\of(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\tryOf(static fn () => $value, $noneValue, strict: $strict));
}

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

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

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

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

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

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

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

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

0 comments on commit 630f918

Please sign in to comment.