2024-07-26 22:10:02 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace Test;
|
|
|
|
|
2024-07-29 17:58:33 +00:00
|
|
|
use BadMethodCallException;
|
2024-07-26 22:10:02 +00:00
|
|
|
use BitBadger\InspiredByFSharp\Option;
|
|
|
|
use InvalidArgumentException;
|
2024-07-28 01:20:35 +00:00
|
|
|
use PhpOption\{None, Some};
|
2024-07-26 22:10:02 +00:00
|
|
|
use PHPUnit\Framework\Attributes\TestDox;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unit tests for the Option class
|
|
|
|
*/
|
|
|
|
class OptionTest extends TestCase
|
|
|
|
{
|
2024-10-01 02:59:46 +00:00
|
|
|
#[TestDox('Value succeeds for Some')]
|
|
|
|
public function testValueSucceedsForSome(): void
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
|
|
|
$it = Option::Some(9);
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isSome, 'The option should have been "Some"');
|
|
|
|
$this->assertEquals(9, $it->value, 'The value was incorrect');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
2024-10-01 02:59:46 +00:00
|
|
|
#[TestDox('Value fails for None')]
|
|
|
|
public function testValueFailsForNone(): void
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
2024-10-01 02:59:46 +00:00
|
|
|
Option::None()->value;
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('IsNone succeeds with None')]
|
|
|
|
public function testIsNoneSucceedsWithNone(): void
|
|
|
|
{
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue(Option::None()->isNone, '"None" should return true');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('IsNone succeeds with Some')]
|
|
|
|
public function testIsNoneSucceedsWithSome(): void
|
|
|
|
{
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertFalse(Option::Some(8)->isNone, '"Some" should return false');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('IsSome succeeds with None')]
|
|
|
|
public function testIsSomeSucceedsWithNone(): void
|
|
|
|
{
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertFalse(Option::None()->isSome, '"None" should return false');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('IsSome succeeds with Some')]
|
|
|
|
public function testIsSomeSucceedsWithSome(): void
|
|
|
|
{
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue(Option::Some('boo')->isSome, '"Some" should return true');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
2024-07-28 18:15:24 +00:00
|
|
|
#[TestDox('GetOrDefault succeeds with None')]
|
|
|
|
public function testGetOrDefaultSucceedsWithNone(): void
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertEquals('yes', Option::None()->getOrDefault('yes'), 'Value should have been default');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
2024-07-28 18:15:24 +00:00
|
|
|
#[TestDox('GetOrDefault succeeds with Some')]
|
|
|
|
public function testGetOrDefaultSucceedsWithSome(): void
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertEquals('no', Option::Some('no')->getOrDefault('yes'), 'Value should have been from option');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('GetOrCall succeeds with None')]
|
|
|
|
public function testGetOrCallSucceedsWithNone(): void
|
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$value = Option::None()->getOrCall(new class { public function __invoke(): string { return 'called'; } });
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('GetOrCall succeeds with Some')]
|
|
|
|
public function testGetOrCallSucceedsWithSome(): void
|
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$value = Option::Some('passed')->getOrCall(
|
|
|
|
new class { public function __invoke(): string { return 'called'; } });
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
|
|
|
|
}
|
|
|
|
|
2024-07-29 17:58:33 +00:00
|
|
|
#[TestDox('GetOrThrow succeeds with Some')]
|
|
|
|
public function testGetOrThrowSucceedsWithSome(): void
|
|
|
|
{
|
|
|
|
$value = Option::Some('no throw')->getOrThrow(fn() => new BadMethodCallException('oops'));
|
|
|
|
$this->assertEquals('no throw', $value, 'The "Some" value should have been returned');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('GetOrThrow succeeds with None')]
|
|
|
|
public function testGetOrThrowSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$this->expectException(BadMethodCallException::class);
|
|
|
|
Option::None()->getOrThrow(fn() => new BadMethodCallException('oops'));
|
|
|
|
}
|
|
|
|
|
2024-07-29 02:50:59 +00:00
|
|
|
#[TestDox('Bind succeeds with None')]
|
|
|
|
public function testBindSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$original = Option::None();
|
|
|
|
$bound = $original->bind(fn($it) => Option::Some('value'));
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($bound->isNone, 'The option should have been None');
|
2024-07-29 02:50:59 +00:00
|
|
|
$this->assertSame($original, $bound, 'The same None instance should have been returned');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Bind succeeds with Some and Some')]
|
|
|
|
public function testBindSucceedsWithSomeAndSome(): void
|
|
|
|
{
|
|
|
|
$bound = Option::Some('hello')->bind(fn($it) => Option::Some('goodbye'));
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($bound->isSome, 'The option should have been Some');
|
|
|
|
$this->assertEquals('goodbye', $bound->value, 'The bound function was not called');
|
2024-07-29 02:50:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Bind succeeds with Some and None')]
|
|
|
|
public function testBindSucceedsWithSomeAndNone(): void
|
|
|
|
{
|
|
|
|
$bound = Option::Some('greetings')->bind(fn($it) => Option::None());
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($bound->isNone, 'The option should have been None');
|
2024-07-29 02:50:59 +00:00
|
|
|
}
|
|
|
|
|
2024-07-29 17:58:33 +00:00
|
|
|
#[TestDox('Contains succeeds with None')]
|
|
|
|
public function testContainsSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$this->assertFalse(Option::None()->contains(null), '"None" should always return false');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Contains succeeds with Some when strictly equal')]
|
|
|
|
public function testContainsSucceedsWithSomeWhenStrictlyEqual(): void
|
|
|
|
{
|
|
|
|
$this->assertTrue(Option::Some(3)->contains(3), '"Some" with strict equality should be true');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Contains succeeds with Some when strictly unequal')]
|
|
|
|
public function testContainsSucceedsWithSomeWhenStrictlyUnequal(): void
|
|
|
|
{
|
|
|
|
$this->assertFalse(Option::Some('3')->contains(3), '"Some" with strict equality should be false');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Contains succeeds with Some when loosely equal')]
|
|
|
|
public function testContainsSucceedsWithSomeWhenLooselyEqual(): void
|
|
|
|
{
|
|
|
|
$this->assertTrue(Option::Some('3')->contains(3, strict: false), '"Some" with loose equality should be true');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Contains succeeds with Some when loosely unequal')]
|
|
|
|
public function testContainsSucceedsWithSomeWhenLooselyUnequal(): void
|
|
|
|
{
|
|
|
|
$this->assertFalse(Option::Some('3')->contains(4, strict: false), '"Some" with loose equality should be false');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Exists succeeds with Some when matching')]
|
|
|
|
public function testExistsSucceedsWithSomeWhenMatching(): void
|
|
|
|
{
|
|
|
|
$this->assertTrue(Option::Some(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Exists succeeds with Some when not matching')]
|
|
|
|
public function testExistsSucceedsWithSomeWhenNotMatching(): void
|
|
|
|
{
|
|
|
|
$this->assertFalse(Option::Some(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Exists succeeds with None')]
|
|
|
|
public function testExistsSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$this->assertFalse(Option::None()->exists(fn($it) => true), 'Exists should have returned false');
|
|
|
|
}
|
|
|
|
|
2024-07-26 22:10:02 +00:00
|
|
|
#[TestDox('Map succeeds with None')]
|
|
|
|
public function testMapSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$tattle = new class { public bool $called = false; };
|
|
|
|
$none = Option::None();
|
2024-07-28 18:15:24 +00:00
|
|
|
$mapped = $none->map(function () use ($tattle)
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
|
|
|
$tattle->called = true;
|
|
|
|
return 'hello';
|
2024-07-28 18:15:24 +00:00
|
|
|
});
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($mapped->isNone, 'The mapped option should be "None"');
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertSame($none, $mapped, 'The same "None" instance should have been returned');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Map succeeds with Some')]
|
|
|
|
public function testMapSucceedsWithSome(): void
|
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2));
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($mapped->isSome, 'The mapped option should be "Some"');
|
|
|
|
$this->assertEquals('abc abc ', $mapped->value, 'The mapping function was not called correctly');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Map fails with Some when mapping is null')]
|
|
|
|
public function testMapFailsWithSomeWhenMappingIsNull(): void
|
|
|
|
{
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
2024-07-28 18:15:24 +00:00
|
|
|
Option::Some('oof')->map(fn($it) => null);
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Iter succeeds with None')]
|
|
|
|
public function testIterSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$target = new class { public mixed $called = null; };
|
2024-07-28 18:15:24 +00:00
|
|
|
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertNull($target->called, 'The function should not have been called');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Iter succeeds with Some')]
|
|
|
|
public function testIterSucceedsWithSome(): void
|
|
|
|
{
|
|
|
|
$target = new class { public mixed $called = null; };
|
2024-07-28 18:15:24 +00:00
|
|
|
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertEquals(33, $target->called, 'The function should have been called with the "Some" value');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Filter succeeds with None')]
|
|
|
|
public function testFilterSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$tattle = new class { public bool $called = false; };
|
|
|
|
$none = Option::None();
|
2024-07-28 18:15:24 +00:00
|
|
|
$filtered = $none->filter(function () use ($tattle)
|
2024-07-26 22:10:02 +00:00
|
|
|
{
|
|
|
|
$tattle->called = true;
|
|
|
|
return true;
|
2024-07-28 18:15:24 +00:00
|
|
|
});
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($filtered->isNone, 'The filtered option should have been "None"');
|
2024-07-26 22:10:02 +00:00
|
|
|
$this->assertFalse($tattle->called, 'The callable should not have been called');
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertSame($none, $filtered, 'The "None" instance returned should have been the one passed');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Filter succeeds with Some when true')]
|
|
|
|
public function testFilterSucceedsWithSomeWhenTrue(): void
|
|
|
|
{
|
|
|
|
$some = Option::Some(12);
|
2024-07-28 18:15:24 +00:00
|
|
|
$filtered = $some->filter(fn($it) => $it % 2 === 0);
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($filtered->isSome, 'The filtered option should have been "Some"');
|
|
|
|
$this->assertEquals(12, $filtered->value, 'The filtered option value is incorrect');
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertSame($some, $filtered, 'The same "Some" instance should have been returned');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Filter succeeds with Some when false')]
|
|
|
|
public function testFilterSucceedsWithSomeWhenFalse(): void
|
|
|
|
{
|
|
|
|
$some = Option::Some(23);
|
2024-07-28 18:15:24 +00:00
|
|
|
$filtered = $some->filter(fn($it) => $it % 2 === 0);
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($filtered->isNone, 'The filtered option should have been "None"');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Unwrap succeeds with None')]
|
|
|
|
public function testUnwrapSucceedsWithNone(): void
|
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertNull(Option::None()->unwrap(), '"None" should return null');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Unwrap succeeds with Some')]
|
|
|
|
public function testUnwrapSucceedsWithSome(): void
|
|
|
|
{
|
2024-07-28 18:15:24 +00:00
|
|
|
$this->assertEquals('boy howdy', Option::Some('boy howdy')->unwrap(), '"Some" should return its value');
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|
2024-07-27 17:03:54 +00:00
|
|
|
|
|
|
|
#[TestDox('Tap succeeds with Some')]
|
|
|
|
public function testTapSucceedsWithSome(): void
|
|
|
|
{
|
|
|
|
$value = '';
|
|
|
|
$original = Option::Some('testing');
|
2024-07-28 18:15:24 +00:00
|
|
|
$tapped = $original->tap(
|
2024-10-01 02:59:46 +00:00
|
|
|
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
|
2024-07-27 17:03:54 +00:00
|
|
|
$this->assertEquals('testing', $value, 'The tapped function was not called');
|
|
|
|
$this->assertSame($original, $tapped, 'The same option should have been returned');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Tap succeeds with None')]
|
|
|
|
public function testTapSucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$value = '';
|
|
|
|
$original = Option::None();
|
2024-07-28 18:15:24 +00:00
|
|
|
$tapped = $original->tap(
|
2024-10-01 02:59:46 +00:00
|
|
|
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
|
2024-07-27 17:03:54 +00:00
|
|
|
$this->assertEquals('none', $value, 'The tapped function was not called');
|
|
|
|
$this->assertSame($original, $tapped, 'The same option should have been returned');
|
|
|
|
}
|
2024-07-28 18:15:24 +00:00
|
|
|
|
2024-07-29 17:58:33 +00:00
|
|
|
#[TestDox('ToArray succeeds with Some')]
|
|
|
|
public function testToArraySucceedsWithSome(): void
|
|
|
|
{
|
|
|
|
$arr = Option::Some('15')->toArray();
|
|
|
|
$this->assertNotNull($arr, 'The array should not have been null');
|
|
|
|
$this->assertEquals(['15'], $arr, 'The array was not created correctly');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('ToArray succeeds with None')]
|
|
|
|
public function testToArraySucceedsWithNone(): void
|
|
|
|
{
|
|
|
|
$arr = Option::None()->toArray();
|
|
|
|
$this->assertNotNull($arr, 'The array should not have been null');
|
|
|
|
$this->assertEmpty($arr, 'The array should have been empty');
|
|
|
|
}
|
|
|
|
|
2024-07-28 18:15:24 +00:00
|
|
|
#[TestDox('ToPhpOption succeeds for Some')]
|
|
|
|
public function testToPhpOptionSucceedsForSome(): void
|
|
|
|
{
|
|
|
|
$opt = Option::Some('php')->toPhpOption();
|
|
|
|
$this->assertNotNull($opt, 'The PhpOption should not have been null');
|
|
|
|
$this->assertInstanceOf('PhpOption\Some', $opt, 'The PhpOption should have been "Some"');
|
|
|
|
$this->assertTrue($opt->isDefined(), 'There should have been a value for the PhpOption');
|
|
|
|
$this->assertEquals('php', $opt->get(), 'The value was not correct');
|
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('ToPhpOption succeeds for None')]
|
|
|
|
public function testToPhpOptionSucceedsForNone(): void
|
|
|
|
{
|
|
|
|
$opt = Option::None()->toPhpOption();
|
|
|
|
$this->assertNotNull($opt, 'The PhpOption should not have been null');
|
|
|
|
$this->assertInstanceOf('PhpOption\None', $opt, 'The PhpOption should have been "None"');
|
|
|
|
$this->assertFalse($opt->isDefined(), 'There should not have been a value for the PhpOption');
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testSomeSucceedsWithValue(): void
|
|
|
|
{
|
|
|
|
$it = Option::Some('hello');
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isSome, 'The option should have been "Some"');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testSomeFailsWithNull(): void
|
|
|
|
{
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
|
|
Option::Some(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testNoneSucceeds(): void
|
|
|
|
{
|
|
|
|
$it = Option::None();
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isNone, 'The option should have been "None"');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testOfSucceedsWithNull(): void
|
|
|
|
{
|
|
|
|
$it = Option::of(null);
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isNone, '"null" should have created a "None" option');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testOfSucceedsWithNonNull(): void
|
|
|
|
{
|
|
|
|
$it = Option::of('test');
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isSome, 'A non-null value should have created a "Some" option');
|
|
|
|
$this->assertEquals('test', $it->value, 'The value was not assigned correctly');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Of succeeds with PhpOption\Some')]
|
|
|
|
public function testOfSucceedsWithPhpOptionSome(): void
|
|
|
|
{
|
|
|
|
$it = Option::of(Some::create('something'));
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isSome, 'A "Some" PhpOption should have created a "Some" option');
|
|
|
|
$this->assertEquals('something', $it->value, 'The value was not assigned correctly');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[TestDox('Of succeeds with PhpOption\None')]
|
|
|
|
public function testOfSucceedsWithPhpOptionNone(): void
|
|
|
|
{
|
|
|
|
$it = Option::of(None::create());
|
2024-10-01 02:59:46 +00:00
|
|
|
$this->assertTrue($it->isNone, 'A "None" PhpOption should have created a "None" option');
|
2024-07-28 18:15:24 +00:00
|
|
|
}
|
2024-07-26 22:10:02 +00:00
|
|
|
}
|