Skip to content

Commit

Permalink
Revert "Revert "feature: Add Result\ify()""
Browse files Browse the repository at this point in the history
This reverts commit b2c07d2.
  • Loading branch information
mathroc committed Sep 26, 2024
1 parent 8a7d3c3 commit b44bd58
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 16 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"psr-4": {
"TH\\Maybe\\": "src/"
},
"files": ["src/functions/option.php", "src/functions/result.php"]
"files": ["src/functions/option.php", "src/functions/result.php", "src/functions/internal.php"]
},
"autoload-dev": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<exclude name="SlevomatCodingStandard.Functions.DisallowNamedArguments" />
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInCall" />
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInDeclaration" />
<exclude name="SlevomatCodingStandard.Functions.DisallowTrailingCommaInClosureUse" />
<exclude name="SlevomatCodingStandard.Functions.UnusedParameter" />
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedClassNameInAnnotation" />
<exclude name="SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions" />
Expand Down
25 changes: 17 additions & 8 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace TH\Maybe\Option;

use TH\DocTest\Attributes\ExamplesSetup;
use TH\Maybe\Internal;
use TH\Maybe\Option;
use TH\Maybe\Result;
use TH\Maybe\Tests\Helpers\IgnoreUnusedResults;
Expand Down Expand Up @@ -114,7 +115,9 @@ 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 Down Expand Up @@ -154,7 +157,7 @@ function tryOf(
* ```
*
* @template U
* @param callable():U $callback
* @param callable(mixed...):U $callback
* @return \Closure(mixed...):Option<U>
*/
function ify(callable $callback, mixed $noneValue = null, bool $strict = true): \Closure
Expand Down Expand Up @@ -194,25 +197,31 @@ function ify(callable $callback, mixed $noneValue = null, bool $strict = true):
* ```
*
* @template U
* @param callable():U $callback
* @template E of \Throwable
* @param callable(mixed...):U $callback
* @param class-string<E> $exceptionClass
* @param class-string<E> $additionalExceptionClasses
* @return \Closure(mixed...):Option<U>
*/
function tryIfy(
callable $callback,
mixed $noneValue = null,
bool $strict = true,
string $exceptionClass = \Exception::class,
string ...$additionalExceptionClasses,
): \Closure
{
return static function (...$args) use ($callback, $noneValue, $strict, $exceptionClass): mixed {
return static function (...$args) use (
$callback,
$noneValue,
$strict,
$exceptionClass,
$additionalExceptionClasses,
): mixed {
try {
return Option\fromValue($callback(...$args), $noneValue, $strict);
} catch (\Throwable $th) {
if (\is_a($th, $exceptionClass)) {
return Option\none();
}

throw $th;
return Internal\trap($th, Option\none(...), $exceptionClass, ...$additionalExceptionClasses);
}
};
}
Expand Down
84 changes: 77 additions & 7 deletions src/functions/result.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace TH\Maybe\Result;

use TH\DocTest\Attributes\ExamplesSetup;
use TH\Maybe\Internal;
use TH\Maybe\Option;
use TH\Maybe\Result;
use TH\Maybe\Tests\Helpers\IgnoreUnusedResults;
Expand Down Expand Up @@ -79,24 +80,93 @@ function err(mixed $value): Result\Err
* @template E of \Throwable
* @param callable(mixed...):U $callback
* @param class-string<E> $exceptionClass
* @param class-string<E> $additionalExceptionClasses
* @return Result<U,E>
* @throws \Throwable
*/
#[ExamplesSetup(IgnoreUnusedResults::class)]
function trap(callable $callback, string $exceptionClass = \Exception::class): Result
{
function trap(
callable $callback,
string $exceptionClass = \Exception::class,
string ...$additionalExceptionClasses,
): Result {
try {
/** @var Result<U,E> */
return Result\ok($callback());
} catch (\Throwable $th) {
if (\is_a($th, $exceptionClass)) {
return Result\err($th);
}

throw $th;
/** @var Result\Err<E> */
return Internal\trap($th, Result\err(...), $exceptionClass, ...$additionalExceptionClasses);
}
}

/**
* Wrap a callable into one that transforms its returned value or thrown exception
* into a `Result` like `Result\trap()` does, but without executing it.
*
* # Examples
*
* Successful execution:
*
* ```
* self::assertEq(Result\ok(3), Result\ify(fn () => 3)());
* ```
*
* Checked exception:
*
* ```
* $x = Result\ify(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\ify(fn () => 1/0)();
* // @throws DivisionByZeroError Division by zero
* ```
*
* Result-ify `strtotime()`:
*
* ```
* $strtotime = Result\ify(
* static fn (...$args)
* => \strtotime(...$args)
* ?: throw new \RuntimeException("Could not convert string to time"),
* );
*
* self::assertEq($strtotime("2015-09-21 UTC midnight")->unwrap(), 1442793600);
*
* $r = $strtotime("nope");
* self::assertTrue($r->isErr());
* $r->unwrap(); // @throws RuntimeException Could not convert string to time
* ```
*
* @template U
* @template E of \Throwable
* @param callable(mixed...):U $callback
* @param class-string<E> $exceptionClass
* @param class-string<E> $additionalExceptionClasses
* @return \Closure(mixed...):Result<U,E>
*/
#[ExamplesSetup(IgnoreUnusedResults::class)]
function ify(
callable $callback,
string $exceptionClass = \Exception::class,
string ...$additionalExceptionClasses,
): \Closure {
return static function (...$args) use ($callback, $exceptionClass, $additionalExceptionClasses): Result
{
try {
return Result\ok($callback(...$args));
} catch (\Throwable $th) {
/** @var Result\Err<E> */
return Internal\trap($th, Result\err(...), $exceptionClass, ...$additionalExceptionClasses);
}
};
}

/**
* Converts from `Result<Result<T, E>, E>` to `Result<T, E>`.
*
Expand Down
66 changes: 66 additions & 0 deletions tests/Unit/Result/IfyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?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 IfyTest extends TestCase
{
use Provider\Values;

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

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

public function testIfyCheckedException(): void
{
$this->expectExceptionObject(
new \Exception(
"Failed to parse time string (nope) at position 0 (n): The timezone could not be found in the database",
),
);

Result\ify(
// @phpstan-ignore-next-line
static fn () => new \DateTimeImmutable("nope"),
)()->unwrap();
}

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

/**
* @dataProvider values
*/
public function testIfyWithArguments(mixed $value): void
{
$fileGetContents = Result\ify(
callback: static fn (string $filename): string => match ($content = \file_get_contents($filename)) {
false => throw new \RuntimeException("Can't get content from $filename"),
default => $content,
},
);

Assert::assertIsCallable($fileGetContents);
}
}

0 comments on commit b44bd58

Please sign in to comment.