diff --git a/src/None.php b/src/None.php index 275b4e2..6db535c 100644 --- a/src/None.php +++ b/src/None.php @@ -3,10 +3,12 @@ namespace Frej\Optional; use Frej\Optional\Exception\OptionNoneUnwrappedException; +use Throwable; /** * @template T * @extends Option + * @internal */ class None extends Option { @@ -93,7 +95,7 @@ public function unwrapInto(callable $callback, null|string|\Throwable $error = n */ public function unwrapIntoOr(callable $callback, mixed $default): void { - $this->unwrapOr($default); + $callback($this->unwrapOr($default)); } /** @@ -101,7 +103,15 @@ public function unwrapIntoOr(callable $callback, mixed $default): void */ public function filter(mixed $predicate): Option { - return None::make(); + return $this; + } + + /** + * @inheritdoc + */ + public function filterInto(callable $callback, mixed $predicate): void + { + $callback($this); } /** @@ -109,7 +119,7 @@ public function filter(mixed $predicate): Option */ public function map(callable $transformer): Option { - return None::make(); + return $this; } /** @@ -122,4 +132,14 @@ public function mapOr(callable $transformer, mixed $default): Option } return Some::make($default); } + + public function mapInto(callable $callback, callable $transformer): void + { + $callback($this->map($transformer)); + } + + public function mapIntoOr(callable $callback, callable $transformer, mixed $default): void + { + $callback($this->mapOr($transformer, $default)); + } } diff --git a/src/Option.php b/src/Option.php index a4af8be..e5a081c 100644 --- a/src/Option.php +++ b/src/Option.php @@ -114,13 +114,24 @@ abstract public function unwrapIntoOr(callable $callback, mixed $default): void; /** * Filter Some(T) using a predicate * - * Calls the provided predicate function on the contained value t if the Option is Some(t), and returns Some(t) if the function returns true; otherwise, returns None + * Calls the provided predicate function on the contained value to check if the Option is Some(T), and returns Some(T) if the function returns true; otherwise, returns None * - * @param T|callable(T): bool $predicate predicate Provided predicate lambda. If returns true, returned value will be Some(T), otherwise None + * @param T|callable(T): bool $predicate Provided predicate lambda. If returns true, returned value will be Some(T), otherwise None * @return Option */ abstract public function filter(mixed $predicate): Option; + /** + * Filter Some(T) using a predicate + * + * Calls the provided predicate function on the contained value to check if the Option is Some(T), and retuns Some(T) if the function returns treu; otherwise returns None + * + * @param callable(Option): void $callback + * @param T|callable(T): bool $predicate Provided predcate lambda. If returns true, returned value will be Some(T), + * otherwise None + */ + abstract public function filterInto(callable $callback, mixed $predicate): void; + /** * Tranform Option to Option using provided the function * @@ -132,6 +143,17 @@ abstract public function filter(mixed $predicate): Option; */ abstract public function map(callable $transformer): Option; + /** + * Tranform Option to Option using provided the function + * + * Leaves None unchanged + * + * @template U + * @param callable(Option): void $callback + * @param callable(T): U $transformer + */ + abstract public function mapInto(callable $callback, callable $transformer): void; + /** * Tranforms Option to Option using the provided function, uses `$default` value on None * @@ -142,6 +164,17 @@ abstract public function map(callable $transformer): Option; */ abstract public function mapOr(callable $transformer, mixed $default): Option; + /** + * Tranforms Option to Option using the provided function, uses `$default` value on None + * and inputs the value into the provided callback + * + * @template U + * @param callable(Option): void $callback + * @param callable(T): U $transformer + * @param U|callable(): U $default fallback value + */ + abstract public function mapIntoOr(callable $callback, callable $transformer, mixed $default): void; + /** * Unwraps inner value into $dst if $self is Some. * diff --git a/src/Some.php b/src/Some.php index 101e353..1fcf039 100644 --- a/src/Some.php +++ b/src/Some.php @@ -5,6 +5,7 @@ /** * @template T * @extends Option + * @internal */ class Some extends Option { @@ -101,6 +102,15 @@ public function filter(mixed $predicate): Option return None::make(); } + /** + * @inheritdoc + */ + public function filterInto(callable $callback, mixed $predicate): void + { + $callback($this->filter($predicate)); + } + + /** * @inheritdoc */ @@ -116,4 +126,20 @@ public function mapOr(callable $transformer, mixed $default): Option { return self::make($transformer($this->val)); } + + /** + * @inheritdoc + */ + public function mapInto(callable $callback, callable $transformer): void + { + $callback($this->map($transformer)); + } + + /** + * @inheritdoc + */ + public function mapIntoOr(callable $callback, callable $transformer, mixed $default): void + { + $callback($this->mapOr($transformer, $default)); + } } diff --git a/tests/NoneTest.php b/tests/NoneTest.php new file mode 100644 index 0000000..ae9f11f --- /dev/null +++ b/tests/NoneTest.php @@ -0,0 +1,180 @@ +assertTrue($option->isEmpty()); + } + + public function testIsSome(): void + { + $option = None::make(); + + $this->assertFalse($option->isSome()); + } + + public function testIsNone(): void + { + $option = None::make(); + + $this->assertTrue($option->isNone()); + } + + public function testThrowsOnUnwrap(): void + { + $option = None::make(); + $this->expectException(OptionNoneUnwrappedException::class); + $option->unwrap(); + } + + public function testThrowsCustomError(): void + { + $option = None::make(); + $this->expectException(OptionNoneUnwrappedException::class); + $this->expectExceptionMessage('my test msg'); + $option->unwrap('my test msg'); + } + + public function testThrowsCustomException(): void + { + $option = None::make(); + + $exception = new class () extends \Exception {}; + + $this->expectException($exception::class); + $option->unwrap($exception); + } + + public function testUnwrapOr(): void + { + $option = None::make(); + + $default = 'hallo'; + + $r = $option->unwrapOr($default); + $this->assertEquals($default, $r); + + $r = $option->unwrapOr(static fn () => $default); + $this->assertEquals($default, $r); + } + + public function testUnwrapOrNull(): void + { + $option = None::make(); + + $this->assertNull($option->unwrapOrNull()); + } + + public function testUnwrapInto(): void + { + $option = None::make(); + + $this->expectException(OptionNoneUnwrappedException::class); + $option->unwrapInto(fn () => $this->assertFalse(true, 'unwrapInto should not call the callback for None')); + } + + public function testUnwrapIntoOr(): void + { + $option = None::make(); + + $option->unwrapIntoOr(function ($v) { + $this->assertEquals('a', $v); + }, 'a'); + } + + public function testFilter(): void + { + $option = None::make(); + + $this->assertInstanceOf(None::class, $option->filter('whatever')); + } + + public function testFiterInto(): void + { + $option = None::make(); + + $option->filterInto( + callback: function ($v) { + $this->assertInstanceOf(None::class, $v); + }, + predicate: 'whatever' + ); + } + + public function testMap(): void + { + $option = None::make(); + + $this->assertInstanceOf(None::class, $option->map(fn () => $this->assertFalse(true, 'map should not call the transformer for None'))); + } + + public function testMapOr(): void + { + $option = None::make(); + + $r = $option->mapOr(fn () => $this->assertFalse(true, 'mapOr should not call the transformer for None'), true); + + $this->assertInstanceOf(Some::class, $r); + $this->assertEquals(true, $r->unwrap()); + } + + public function testMapInto(): void + { + $option = None::make(); + + $option->mapInto( + callback: fn ($v) => $this->assertInstanceOf(None::class, $v), + transformer: fn () => $this->assertFalse(true, 'mapInto should not call the transformer for None') + ); + } + + public function testMapIntoOr(): void + { + $option = None::make(); + + $option->mapIntoOr( + callback: function ($v) { + $this->assertInstanceOf(Some::class, $v); + $this->assertEquals(true, $v->unwrap()); + }, + transformer: fn ($v) => $this->assertFalse(true, 'mapIntoOr should not call the transformer for None'), + default: 'me' + ); + } + + public function testSingletonNoneObjectIsCreatedWhenNoneCalled(): void + { + $noneOption1 = None::make(); + $noneOption2 = None::make(); + $this->assertSame($noneOption1, $noneOption2); + $this->assertTrue($noneOption1->isNone()); + } + + public function testNonePropertyIsInstantiatedAfterConstructCall(): void + { + $reflection = new ReflectionClass(None::class); + $noneProperty = $reflection->getProperty('singleton'); + $noneProperty->setAccessible(true); + + // Unset none for the next test + $noneProperty->setValue(null, null); + + // Check after calling None method + $noneOption = None::make(); + $this->assertInstanceOf(None::class, $noneProperty->getValue($noneOption)); + + // Unset none for the next tests + $noneProperty->setValue(null, null); + } +} diff --git a/tests/OptionTest.php b/tests/OptionTest.php index 8a4e743..13757f0 100644 --- a/tests/OptionTest.php +++ b/tests/OptionTest.php @@ -2,16 +2,17 @@ namespace Frej\Optional\Tests; -use Frej\Optional\Exception\OptionNoneUnwrappedException; use Frej\Optional\Option; +use Frej\Optional\Some; +use Frej\Optional\None; use PHPUnit\Framework\TestCase; -use ReflectionClass; class OptionTest extends TestCase { public function testOptionSomeIsNotNone(): void { $option = Option::Some(null); + $this->assertInstanceOf(Some::class, $option); $this->assertFalse($option->isNone()); $this->assertTrue($option->isSome()); } @@ -19,78 +20,20 @@ public function testOptionSomeIsNotNone(): void public function testOptionNoneIsNotSome(): void { $option = Option::None(); + $this->assertInstanceOf(None::class, $option); $this->assertTrue($option->isNone()); $this->assertFalse($option->isSome()); } - public function testOptionNoneThrowsOnUnwrap(): void - { - $option = Option::None(); - $this->expectException(OptionNoneUnwrappedException::class); - $option->unwrap(); - } - - public function testOptionNoneThrowsOnExpect(): void - { - $option = Option::None(); - $this->expectException(OptionNoneUnwrappedException::class); - $option->expect("MyMessage"); - $this->expectExceptionMessage("MyMessage"); - } - - /** - * Tests if the Option object will be created with a single value - * - * @return void - */ - public function testOptionCreatedWithSingleValue(): void - { - $option = Option::Some('single value'); - $this->assertTrue($option->isSome()); - $this->assertSame('single value', $option->unwrap()); - $this->assertSame('single value', $option->expect("MyMessage")); - } - - /** - * Tests if singleton None object is created when None method is called - * - * @return void - */ - public function testSingletonNoneObjectIsCreatedWhenNoneCalled(): void - { - $noneOption1 = Option::None(); - $noneOption2 = Option::None(); - $this->assertSame($noneOption1, $noneOption2); - $this->assertTrue($noneOption1->isNone()); - } - /** - * Tests if the $none property is instantiated after construct and None call if - * it was not instantiated yet - * - * @return void - */ - public function testNonePropertyIsInstantiatedAfterConstructAndNoneCall(): void + public function testOptionLetSome(): void { - $reflection = new ReflectionClass(Option::class); - $noneProperty = $reflection->getProperty('none'); - $noneProperty->setAccessible(true); - - // Unset none for the test - $noneProperty->setValue(null, null); - - // Check after constructing Some option - $someOption = Option::Some('some value'); - $this->assertInstanceOf(Option::class, $noneProperty->getValue($someOption)); - - // Unset none for the next test - $noneProperty->setValue(null, null); - - // Check after calling None method - $noneOption = Option::None(); - $this->assertInstanceOf(Option::class, $noneProperty->getValue($noneOption)); + $r = Option::letSome($a, Option::Some('a')); + $this->assertTrue($r, 'Option::letSome() should not return false when the option is Some'); + $this->assertEquals('a', $a); - // Unset none for the next tests - $noneProperty->setValue(null, null); + $r = Option::letSome($b, Option::None()); + $this->assertFalse($r, 'Option::letSome() should not return true when the option is None'); + $this->assertFalse(isset($b), 'variable should not be set after calling Option::letSome() with None'); } } diff --git a/tests/SomeTest.php b/tests/SomeTest.php new file mode 100644 index 0000000..69ad9dd --- /dev/null +++ b/tests/SomeTest.php @@ -0,0 +1,211 @@ +assertFalse($option->isEmpty()); + + $option = Some::make(''); + $this->assertTrue($option->isEmpty()); + } + + public function testIsSome(): void + { + $option = Some::make('a'); + + $this->assertTrue($option->isSome()); + } + + public function testIsNone(): void + { + $option = Some::make('a'); + + $this->assertFalse($option->isNone()); + } + + public function testUnwrap(): void + { + $option = Some::make('a'); + $this->assertEquals('a', $option->unwrap()); + } + + public function testUnwrapOr(): void + { + $initial = 'a'; + $option = Some::make($initial); + + $default = 'hallo'; + + $r = $option->unwrapOr($default); + $this->assertEquals($initial, $r); + + $r = $option->unwrapOr(static fn () => $default); + $this->assertEquals($initial, $r); + } + + public function testUnwrapOrNull(): void + { + $option = Some::make('a'); + + $r = $option->unwrapOrNull(); + + $this->assertEquals('a', $r); + } + + public function testUnwrapInto(): void + { + $val = 'a'; + $option = Some::make($val); + + $option->unwrapInto(fn ($v) => $this->assertEquals($val, $v)); + } + + public function testUnwrapIntoOr(): void + { + $val = 'a'; + $option = Some::make($val); + + $option->unwrapIntoOr(function ($v) { + $this->assertEquals('a', $v); + }, 'a'); + } + + public function testFilter(): void + { + $option = Some::make('a'); + + $r = $option->filter('whatever'); + $this->assertInstanceOf(None::class, $r); + + $r = $option->filter('a'); + $this->assertInstanceOf(Some::class, $r); + $this->assertEquals('a', $r->unwrap()); + $this->assertSame($option, $r); + + $r = $option->filter(function ($v) { + $this->assertEquals('a', $v, 'filter should pass the wrapped value into the callback'); + return false; + }); + $this->assertInstanceOf(None::class, $r); + + $r = $option->filter(fn ($v) => true); + $this->assertInstanceOf(Some::class, $r); + $this->assertEquals('a', $r->unwrap()); + $this->assertSame($option, $r); + } + + public function testFilterInto(): void + { + $option = Some::make('a'); + + $option->filterInto( + callback: function ($v) { + $this->assertInstanceOf(None::class, $v); + }, + predicate: 'whatever' + ); + + $option->filterInto( + callback: function ($v) use ($option) { + $this->assertInstanceOf(Some::class, $v); + $this->assertEquals('a', $v->unwrap()); + $this->assertSame($option, $v); + }, + predicate: 'a' + ); + + $option->filterInto( + callback: function ($v) { + $this->assertInstanceOf(None::class, $v); + }, + predicate: function ($v) { + $this->assertEquals('a', $v, 'filter should pass the wrapped value into the callback'); + return false; + } + ); + + $option->filterInto( + callback: function ($v) use ($option) { + $this->assertInstanceOf(Some::class, $v); + $this->assertEquals('a', $v->unwrap()); + $this->assertSame($option, $v); + }, + predicate: fn ($v) => true + ); + } + + public function testMap(): void + { + $option = Some::make('a'); + + $r = $option->map(function ($v) { + $this->assertEquals('a', $v); + return 20; + }); + $this->assertInstanceOf(Some::class, $r); + $this->assertEquals(20, $r->unwrap()); + $this->assertNotSame($option, $r); + } + + public function testMapOr(): void + { + $option = Some::make('a'); + + $r = $option->mapOr(function ($v) { + $this->assertEquals('a', $v); + return 20; + }, 1); + + $this->assertInstanceOf(Some::class, $r); + $this->assertEquals(20, $r->unwrap()); + $this->assertNotSame($option, $r); + } + + public function testMapInto(): void + { + $option = Some::make('a'); + + $option->mapInto( + callback: function ($v) use ($option) { + $this->assertInstanceOf(Some::class, $v); + $this->assertEquals(20, $v->unwrap()); + $this->assertNotSame($option, $v); + }, + transformer: function ($v) { + $this->assertEquals('a', $v); + return 20; + } + ); + } + + public function testMapIntoOr(): void + { + $option = Some::make('a'); + + $option->mapIntoOr( + callback: function ($v) use ($option) { + $this->assertInstanceOf(Some::class, $v); + $this->assertEquals(20, $v->unwrap()); + $this->assertNotSame($option, $v); + }, + transformer: function ($v) { + $this->assertEquals('a', $v); + return 20; + }, + default: 'me' + ); + } +}