inspired-by-fsharp/tests/Unit/OptionTest.php

257 lines
9.2 KiB
PHP

<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use PhpOption\{None, Some};
describe('->value', function () {
test('retrieves the value for Some', function () {
expect(Option::Some(9)->value)->toBe(9);
});
test('throws an exception for None', function () {
expect(fn() => Option::None()->value)->toThrow(InvalidArgumentException::class);
});
});
describe('->isNone', function () {
test('returns true for None', function () {
expect(Option::None()->isNone)->toBeTrue();
});
test('returns false for Some', function () {
expect(Option::Some(8)->isNone)->toBeFalse();
});
});
describe('->isSome', function () {
test('returns false for None', function () {
expect(Option::None()->isSome)->toBeFalse();
});
test('returns true for Some', function () {
expect(Option::Some('boo')->isSome)->toBeTrue();
});
});
describe('->getOrDefault()', function () {
test('returns default value for None', function () {
expect(Option::None()->getOrDefault('yes'))->toBe('yes');
});
test('returns option value for Some', function () {
expect(Option::Some('no')->getOrDefault('yes'))->toBe('no');
});
});
describe('->getOrCall()', function () {
test('returns value from callable for None', function () {
expect(Option::None()->getOrCall(fn() => 'called'))->toBe('called');
});
test('returns option value for Some', function () {
expect(Option::Some('passed')->getOrCall(fn() => 'called'))->toBe('passed');
});
});
describe('->getOrThrow()', function () {
test('throws an exception for None', function () {
expect(fn() => Option::None()->getOrThrow(fn() => throw new BadMethodCallException()))
->toThrow(BadMethodCallException::class);
});
test('returns option value for Some', function () {
expect(Option::Some('no throw')->getOrThrow(fn() => throw new BadMethodCallException()))->toBe('no throw');
});
});
describe('->bind()', function () {
test('returns None when binding against None', function () {
$original = Option::None();
$bound = $original->bind(fn($it) => Option::Some('value'));
expect($bound)->isNone->toBeTrue()->and($bound)->toBe($original);
});
test('returns Some when binding against Some with Some', function () {
expect(Option::Some('hello')->bind(fn($it) => Option::Some('goodbye')))
->isSome->toBeTrue()->value->toBe('goodbye');
});
test('returns None when binding against Some with None', function () {
expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone->toBeTrue();
});
});
describe('->contains()', function () {
test('returns false for None', function () {
expect(Option::None()->contains(null))->toBeFalse();
});
test('returns true for Some when strict equality is matched', function () {
expect(Option::Some(3)->contains(3))->toBeTrue();
});
test('returns false for Some when strict equality is not matched', function () {
expect(Option::Some('3')->contains(3))->toBeFalse();
});
test('returns true for Some when loose equality is matched', function () {
expect(Option::Some('3')->contains(3, strict: false))->toBeTrue();
});
test('returns false for Some when loose equality is not matched', function () {
expect(Option::Some('3')->contains(4, strict: false))->toBeFalse();
});
});
describe('->exists()', function () {
test('returns false for None', function () {
expect(Option::None()->exists(fn($it) => true))->toBeFalse();
});
test('returns true for Some and matching condition', function () {
expect(Option::Some(14)->exists(fn($it) => $it < 100))->toBeTrue();
});
test('returns false for Some and non-matching condition', function () {
expect(Option::Some(14)->exists(fn($it) => $it > 100))->toBeFalse();
});
});
describe('->map()', function () {
test('does nothing for None', function () {
$tattle = new class { public bool $called = false; };
$none = Option::None();
$mapped = $none->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
expect($mapped)
->isNone->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($none);
});
test('maps value for Some', function () {
expect(Option::Some('abc ')->map(fn($it) => str_repeat($it, 2)))
->isSome->toBeTrue()->value->toBe('abc abc ');
});
test('throws an exception if mapping returns null', function () {
expect(fn() => Option::Some('oof')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
});
});
describe('->iter()', function () {
test('does nothing for None', function () {
$target = new class { public mixed $called = null; };
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
expect($target->called)->toBeNull();
});
test('iterates for Some', function () {
$target = new class { public mixed $called = null; };
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
expect($target->called)->toBe(33);
});
});
describe('->filter()', function () {
test('does nothing for None', function () {
$tattle = new class { public bool $called = false; };
$none = Option::None();
$filtered = $none->filter(function () use ($tattle)
{
$tattle->called = true;
return true;
});
expect($filtered)
->isNone->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($filtered)->toBe($none);
});
test('returns Some when filter is matched', function () {
$some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
expect($filtered)
->isSome->toBeTrue()
->value->toBe(12)
->and($filtered)->toBe($some);
});
test('returns None when filter is not matched', function () {
expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone)->toBeTrue();
});
});
describe('->unwrap()', function () {
test('returns null for None', function () {
expect(Option::None()->unwrap())->toBeNull();
});
test('returns option value for Some', function () {
expect(Option::Some('boy howdy')->unwrap())->toBe('boy howdy');
});
});
describe('->tap()', function () {
test('is called for None', function () {
$value = '';
$original = Option::None();
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
expect($value)->toBe('none')->and($original)->toBe($tapped);
});
test('is called for Some', function () {
$value = '';
$original = Option::Some('testing');
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
expect($value)->toBe('testing')->and($original)->toBe($tapped);
});
});
describe('->toArray()', function () {
test('returns empty array for None', function () {
expect(Option::None()->toArray())->not->toBeNull()->toBeEmpty();
});
test('returns one-item array for Some', function () {
expect(Option::Some('15')->toArray())->not->toBeNull()->toBe(['15']);
});
});
describe('->toPhpOption()', function () {
test('converts None', function () {
expect(Option::None()->toPhpOption())
->toBeInstanceOf('PhpOption\None')
->not->toBeNull()
->isDefined()->toBeFalse();
});
test('converts Some', function () {
expect(Option::Some('php')->toPhpOption())
->toBeInstanceOf('PhpOption\Some')
->not->toBeNull()
->isDefined()->toBeTrue()
->get()->toBe('php');
});
});
describe('::Some()', function () {
test('creates a Some option when given a value', function () {
expect(Option::Some('hello'))->not->toBeNull()->isSome->toBeTrue();
});
test('throws an exception when given null', function () {
expect(fn() => Option::Some(null))->toThrow(InvalidArgumentException::class);
});
});
describe('::None()', function () {
test('creates a None option', function () {
expect(Option::None())->not->toBeNull()->isNone->toBeTrue();
});
});
describe('::of()', function () {
test('creates a None option when given null', function () {
expect(Option::of(null))->not->toBeNull()->isNone->toBeTrue();
});
test('creates a Some option when given a value', function () {
expect(Option::of('test'))->not->toBeNull()
->isSome->toBeTrue()
->value->toBe('test');
});
test('creates a None option when given PhpOption\None', function () {
expect(Option::of(None::create()))->isNone->toBeTrue();
});
test('creates a Some option when given PhpOption\Some', function () {
expect(Option::of(Some::create('something')))->isSome->toBeTrue()->value->toBe('something');
});
});