Skip to content

Commit 9da2318

Browse files
committed
feature: Add Result\ify()
1 parent 164c3e8 commit 9da2318

File tree

3 files changed

+126
-5
lines changed

3 files changed

+126
-5
lines changed

src/functions/option.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ function of(callable $callback, mixed $noneValue = null, bool $strict = true): O
114114
* ```
115115
*
116116
* @template U
117+
* @template E of \Throwable
117118
* @param callable():U $callback
119+
* @param class-string<E> $exceptionClass
118120
* @return Option<U>
119121
* @throws \Throwable
120122
*/
@@ -154,7 +156,7 @@ function tryOf(
154156
* ```
155157
*
156158
* @template U
157-
* @param callable():U $callback
159+
* @param callable(mixed...):U $callback
158160
* @return \Closure(mixed...):Option<U>
159161
*/
160162
function ify(callable $callback, mixed $noneValue = null, bool $strict = true): \Closure
@@ -194,7 +196,9 @@ function ify(callable $callback, mixed $noneValue = null, bool $strict = true):
194196
* ```
195197
*
196198
* @template U
197-
* @param callable():U $callback
199+
* @template E of \Throwable
200+
* @param callable(mixed...):U $callback
201+
* @param class-string<E> $exceptionClass
198202
* @return \Closure(mixed...):Option<U>
199203
*/
200204
function tryIfy(

src/functions/result.php

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,17 @@ function err(mixed $value): Result\Err
7575
* ```
7676
*
7777
* @template U
78-
* @param callable():U $callback
79-
* @return Result<U,\Throwable>
78+
* @template E of \Throwable
79+
* @param callable(mixed...):U $callback
80+
* @param class-string<E> $exceptionClass
81+
* @return Result<U,E>
8082
* @throws \Throwable
8183
*/
8284
#[ExamplesSetup(IgnoreUnusedResults::class)]
8385
function trap(callable $callback, string $exceptionClass = \Exception::class): Result
8486
{
8587
try {
86-
/** @var Result<U,\Throwable> */
88+
/** @var Result<U,E> */
8789
return Result\ok($callback());
8890
} catch (\Throwable $th) {
8991
if (\is_a($th, $exceptionClass)) {
@@ -94,6 +96,73 @@ function trap(callable $callback, string $exceptionClass = \Exception::class): R
9496
}
9597
}
9698

99+
/**
100+
* Wrap a callable into one that transforms its returned value or thrown exception
101+
* into a `Result` like `Result\trap()` does.
102+
*
103+
* # Examples
104+
*
105+
* Successful execution:
106+
*
107+
* ```
108+
* self::assertEq(Result\ok(3), Result\ify(fn () => 3)());
109+
* ```
110+
*
111+
* Checked exception:
112+
*
113+
* ```
114+
* $x = Result\ify(fn () => new \DateTimeImmutable("2020-30-30 UTC"))();
115+
* self::assertTrue($x->isErr());
116+
* $x->unwrap();
117+
* // @throws Exception Failed to parse time string (2020-30-30 UTC) at position 6 (0): Unexpected character
118+
* ```
119+
*
120+
* Unchecked exception:
121+
*
122+
* ```
123+
* Result\ify(fn () => 1/0)();
124+
* // @throws DivisionByZeroError Division by zero
125+
* ```
126+
*
127+
* Result-ify `strtotime()`:
128+
*
129+
* ```
130+
* $strtotime = Result\ify(
131+
* static fn (...$args)
132+
* => \strtotime(...$args)
133+
* ?: throw new \RuntimeException("Could not convert string to time"),
134+
* );
135+
*
136+
* self::assertEq($strtotime("2015-09-21 UTC midnight")->unwrap(), 1442793600);
137+
*
138+
* $r = $strtotime("nope");
139+
* self::assertTrue($r->isErr());
140+
* $r->unwrap(); // @throws RuntimeException Could not convert string to time
141+
* ```
142+
*
143+
* @template U
144+
* @template E of \Throwable
145+
* @param callable(mixed...):U $callback
146+
* @param class-string<E> $exceptionClass
147+
* @return \Closure(mixed...):Result<U,E>
148+
*/
149+
#[ExamplesSetup(IgnoreUnusedResults::class)]
150+
function ify(callable $callback, string $exceptionClass = \Exception::class): \Closure
151+
{
152+
return static function (...$args) use ($callback, $exceptionClass): Result
153+
{
154+
try {
155+
return Result\ok($callback(...$args));
156+
} catch (\Throwable $th) {
157+
if (\is_a($th, $exceptionClass)) {
158+
return Result\err($th);
159+
}
160+
161+
throw $th;
162+
}
163+
};
164+
}
165+
97166
/**
98167
* Converts from `Result<Result<T, E>, E>` to `Result<T, E>`.
99168
*

tests/Unit/Result/IfyTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace TH\Maybe\Tests\Unit\Result;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use TH\Maybe\Result;
7+
use TH\Maybe\Tests\Assert;
8+
use TH\Maybe\Tests\Provider;
9+
10+
final class IfyTest extends TestCase
11+
{
12+
use Provider\Values;
13+
14+
/**
15+
* @dataProvider values
16+
*/
17+
public function testIfyOk(mixed $value): void
18+
{
19+
$callback = static fn () => $value;
20+
21+
Assert::assertEquals($value, Result\ify($callback)()->unwrap());
22+
}
23+
24+
public function testIfyCheckedException(): void
25+
{
26+
Assert::assertEquals(
27+
new \Exception(
28+
"Failed to parse time string (nope) at position 0 (n): The timezone could not be found in the database",
29+
),
30+
// @phpstan-ignore-next-line
31+
Result\ify(static fn () => new \DateTimeImmutable("nope"))()->unwrapErr(),
32+
);
33+
}
34+
35+
public function testIfyUncheckedException(): void
36+
{
37+
try {
38+
// @phpstan-ignore-next-line
39+
Result\ify(static fn () => 1 / 0)();
40+
Assert::fail("An exception should have been thrown");
41+
} catch (\DivisionByZeroError $ex) {
42+
Assert::assertEquals(
43+
"Division by zero",
44+
$ex->getMessage(),
45+
);
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)