Compare commits
4 Commits
v1.0.0-bet
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cc780956d | |||
| 6779b2c554 | |||
| fad428a4e4 | |||
| 57af645d87 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.idea
|
||||
vendor
|
||||
*-tests.txt
|
||||
|
||||
31
README.md
31
README.md
@@ -6,25 +6,32 @@ This project contains PHP utility classes whose functionality is inspired by the
|
||||
|
||||
This early-stage library currently provides two classes, both of which are designed to wrap values and indicate the state of the action that produced them. `Option<T>` represents a variable that may or may not have a value. `Result<TOK, TError>` represents the result of an action; the "ok" and "error" states both provide a value.
|
||||
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|----------------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some<br>`::None()` for None<br>`::of($value)` _None if `null`_ | `::OK(TOK)` for OK<br>`::Error(TError)` for Error |
|
||||
| **Querying** | `->isSome()`<br>`->isNone()` | `->isOK()`<br>`->isError()` |
|
||||
| **Reading**<br>_throws if called on missing value_ | `->get()` | `->getOK()`<br>`->getError()` |
|
||||
| **Transforming**<br>_still `Option` or `Result`_ | `->map(callable(T): U)` | `->map(callable(TOK): U)`<br>`->mapError(callable(TError): U)` |
|
||||
| **Iterating** | `->iter(callable(T): void)` | `->iter(callable(TOK): void)` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void)` | `->tap(callable(Result<TOK, TError>): void)` |
|
||||
| | `Option<T>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>Replaces exception-based error handling |
|
||||
|---------------------------------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
| **Creating** | `::Some(T)` for Some | `::OK(TOK)` for OK |
|
||||
| | `::None()` for None | `::Error(TError)` for Error |
|
||||
| | `::of($value)` _None if `null`_ | |
|
||||
| **Querying** | `->isSome(): bool` | `->isOK(): bool` |
|
||||
| | `->isNone(): bool` | `->isError(): bool` |
|
||||
| | `->contains(T, $strict = true): bool` | `->contains(TOK, $strict = true): bool` |
|
||||
| | `->exists(callable(T): bool): bool` | `->exists(callable(TOK): bool): bool` |
|
||||
| **Reading**<br> | `->get(): T` | `->getOK(): TOK` |
|
||||
| _all throw if called on missing value_ | | `->getError(): TError` |
|
||||
| **Transforming**<br> | `->map(callable(T): TMapped): Option<TMapped>` | `->map(callable(TOK): TMapped): Result<TMapped, TError>` |
|
||||
| _all still `Option` or `Result`_ | | `->mapError(callable(TError): TMapped): Result<TOK, TMapped>` |
|
||||
| **Iterating** | `->iter(callable(T): void): void` | `->iter(callable(TOK): void): void` |
|
||||
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void): Option<T>` | `->tap(callable(Result<TOK, TError>): void): Result<TOK, TError>` |
|
||||
| **Continued Processing** | `->bind(callable(T): Option<TBound>): Option<TBound>` | `->bind(callable(TOK): Result<TBoundOK, TError>): Result<TBoundOK, TError>` |
|
||||
| **Changing Types** | `->toArray(): T[]` | `->toArray(): TOK[]` |
|
||||
| | | `->toOption(): Option<TOK>` |
|
||||
|
||||
In addition to this, `Option<T>` provides:
|
||||
- `->getOrDefault(T)` will return the Some value if it exists or the given default if the option is None.
|
||||
- `->getOrCall(callable(): mixed)` will call the given function if the option is None. That function may return a value, or may be `void` or `never`.
|
||||
- `->getOrThrow(callable(): Exception)` will return the Some value if it exists, or throw the exception returned by the function if the option is None.
|
||||
- `->filter(callable(T): bool)` will compare a Some value against the callable, and if it returns `true`, will remain Some; if it returns `false`, the value will become None.
|
||||
- `->is(T, $strict = true)` will return `true` if the option is Some and the value matches. Strict equality (the default) uses `===` for the comparison; if strict is set to `false`, the comparison will use `==` instead.
|
||||
- `->unwrap()` will return `null` for None options and the value for Some options.
|
||||
|
||||
`Result<TOK, TError>` also provides:
|
||||
- `toOption()` will transform an OK result to a Some option, and an Error result to a None option.
|
||||
|
||||
Finally, we would be remiss to not acknowledge some really cool prior art in this area - the [PhpOption](https://github.com/schmittjoh/php-option) project. `Option::of` recognizes their options and converts them properly, and `Option<T>` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some<T>` and `None` instances. There is also a [ResultType](https://github.com/GrahamCampbell/Result-Type) project from the same team, though this project's result does not (yet) have any conversion methods for it.
|
||||
|
||||
## The Inspiration
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss"
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.2"
|
||||
"php": "8.2 - 8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11",
|
||||
"phpoption/phpoption": "^1"
|
||||
"phpoption/phpoption": "^1",
|
||||
"pestphp/pest": "^3.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -30,10 +30,15 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Test\\": "./tests"
|
||||
"Tests\\": "./tests"
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": [ "/tests", "/.gitattributes", "/.gitignore", "/.git", "/composer.lock" ]
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2255
composer.lock
generated
2255
composer.lock
generated
File diff suppressed because it is too large
Load Diff
17
phpunit.xml
Normal file
17
phpunit.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace BitBadger\InspiredByFSharp;
|
||||
|
||||
use Error;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -84,21 +85,73 @@ readonly class Option
|
||||
/**
|
||||
* Get the value, or return the value of a callable function
|
||||
*
|
||||
* @template U The return type of the callable provided
|
||||
* @param callable(): U $f The callable function to use for `None` options
|
||||
* @return T|mixed The value if `Some`, the result of the callable if `None`
|
||||
* @template TCalled The return type of the callable provided
|
||||
* @param callable(): TCalled $f The callable function to use for `None` options
|
||||
* @return T|TCalled The value if `Some`, the result of the callable if `None`
|
||||
*/
|
||||
public function getOrCall(callable $f): mixed
|
||||
{
|
||||
return $this->value ?? $f();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value, or throw the
|
||||
* @param callable(): Exception $exFunc A function to construct the exception to throw
|
||||
* @return T The value of the option if `Some`
|
||||
* @throws Exception If the option is `None`
|
||||
*/
|
||||
public function getOrThrow(callable $exFunc): mixed
|
||||
{
|
||||
return $this->value ?? throw $exFunc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a function to this option (railway processing)
|
||||
*
|
||||
* If this option is Some, the function will be called with the option's value. If this option is None, it will be
|
||||
* immediately returned.
|
||||
*
|
||||
* @template TBound The type returned by Some in the bound function
|
||||
* @param callable(T): Option<TBound> $f The function that will receive the Some value; can return a different type
|
||||
* @return Option<TBound> The updated option if the starting value was Some, None otherwise
|
||||
*/
|
||||
public function bind(callable $f): Option
|
||||
{
|
||||
return $this->isNone() ? $this : $f($this->get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the option contain the given value?
|
||||
*
|
||||
* @param T $value The value to be checked
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`); optional, default is true
|
||||
* @return bool True if the value matches, false if not; `None` always returns false
|
||||
*/
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => false,
|
||||
default => $strict ? $this->value === $value : $this->value == $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the value of the option match the given predicate function?
|
||||
*
|
||||
* @param callable(T): bool $f The function to determine whether the value matches
|
||||
* @return bool True if the `Some` value matches the function, false otherwise
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isSome() ? $f($this->value) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map this optional value to another value
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(T): U $f The mapping function
|
||||
* @return Option<U> A `Some` instance with the transformed value if `Some`, `None` otherwise
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(T): TMapped $f The mapping function
|
||||
* @return Option<TMapped> A `Some` instance with the transformed value if `Some`, `None` otherwise
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
@@ -125,25 +178,7 @@ readonly class Option
|
||||
*/
|
||||
public function filter(callable $f): self
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => $this,
|
||||
default => $f($this->value) ? $this : self::None(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the option have the given value?
|
||||
*
|
||||
* @param T $value The value to be checked
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`)
|
||||
* @return bool True if the value matches, false if not; `None` always returns false
|
||||
*/
|
||||
public function is(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => false,
|
||||
default => $strict ? $this->value === $value : $this->value == $value,
|
||||
};
|
||||
return $this->isNone() || $this->exists($f) ? $this : self::None();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +203,16 @@ readonly class Option
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this option into a 0 or 1 item array
|
||||
*
|
||||
* @return T[] An empty array for `None`, a 1-item array for `Some`
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->isSome() ? [$this->value] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this to a PhpOption option
|
||||
*
|
||||
|
||||
@@ -87,12 +87,54 @@ readonly class Result
|
||||
return $this->errorValue->isSome();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a function to this result (railway processing)
|
||||
*
|
||||
* If this result is OK, the function will be called with the OK value of the result. If this result is Error, it
|
||||
* will be immediately returned. This allows for a sequence of functions to proceed on the happy path (OK all the
|
||||
* way), or be shunted off to the exit ramp once an error occurs.
|
||||
*
|
||||
* @template TBoundOK The type returned by OK in the bound function
|
||||
* @param callable(TOK): Result<TBoundOK, TError> $f The function that will receive the OK value; can return a different type
|
||||
* @return Result<TBoundOK, TError> The updated result if the function was successful, an error otherwise
|
||||
*/
|
||||
public function bind(callable $f): Result
|
||||
{
|
||||
return $this->isError() ? $this : $f($this->getOK());
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this result's "OK" value match the given value?
|
||||
*
|
||||
* @param TOK $value The value to be matched
|
||||
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`); optional, default is true
|
||||
* @return bool True if the "OK" value matches the one provided, false otherwise
|
||||
*/
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isError() => false,
|
||||
default => $this->okValue->contains($value, $strict),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the "OK" value of this result match the given predicate function?
|
||||
*
|
||||
* @param callable(TOK): bool $f The function to determine whether the value matches
|
||||
* @return bool True if the OK value matches the function, false otherwise
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isOK() ? $f($this->okValue->get()) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an `OK` result to another, leaving an `Error` result unmodified
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(TOK): U $f The mapping function
|
||||
* @return Result<U, TError> A transformed `OK` instance or the original `Error` instance
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(TOK): TMapped $f The mapping function
|
||||
* @return Result<TMapped, TError> A transformed `OK` instance or the original `Error` instance
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
@@ -102,9 +144,9 @@ readonly class Result
|
||||
/**
|
||||
* Map an `Error` result to another, leaving an `OK` result unmodified
|
||||
*
|
||||
* @template U The type of the mapping function
|
||||
* @param callable(TError): U $f The mapping function
|
||||
* @return Result<TOK, U> A transformed `Error` instance or the original `OK` instance
|
||||
* @template TMapped The type of the mapping function
|
||||
* @param callable(TError): TMapped $f The mapping function
|
||||
* @return Result<TOK, TMapped> A transformed `Error` instance or the original `OK` instance
|
||||
*/
|
||||
public function mapError(callable $f): self
|
||||
{
|
||||
@@ -123,6 +165,16 @@ readonly class Result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this result into a 0 or 1 item array
|
||||
*
|
||||
* @return TOK[] An empty array for `Error`, a 1-item array for `OK`
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->okValue->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `Result`'s `OK` value to an `Option`
|
||||
*
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Test;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use InvalidArgumentException;
|
||||
use PhpOption\{None, Some};
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for the Option class
|
||||
*/
|
||||
class OptionTest extends TestCase
|
||||
{
|
||||
#[TestDox('Get succeeds for Some')]
|
||||
public function testGetSucceedsForSome(): void
|
||||
{
|
||||
$it = Option::Some(9);
|
||||
$this->assertTrue($it->isSome(), 'The option should have been "Some"');
|
||||
$this->assertEquals(9, $it->get(), 'The value was incorrect');
|
||||
}
|
||||
|
||||
#[TestDox('Get fails for None')]
|
||||
public function testGetFailsForNone(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Option::None()->get();
|
||||
}
|
||||
|
||||
#[TestDox('IsNone succeeds with None')]
|
||||
public function testIsNoneSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertTrue(Option::None()->isNone(), '"None" should return true');
|
||||
}
|
||||
|
||||
#[TestDox('IsNone succeeds with Some')]
|
||||
public function testIsNoneSucceedsWithSome(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some(8)->isNone(), '"Some" should return false');
|
||||
}
|
||||
|
||||
#[TestDox('IsSome succeeds with None')]
|
||||
public function testIsSomeSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertFalse(Option::None()->isSome(), '"None" should return false');
|
||||
}
|
||||
|
||||
#[TestDox('IsSome succeeds with Some')]
|
||||
public function testIsSomeSucceedsWithSome(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some('boo')->isSome(), '"Some" should return true');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrDefault succeeds with None')]
|
||||
public function testGetOrDefaultSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertEquals('yes', Option::None()->getOrDefault('yes'), 'Value should have been default');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrDefault succeeds with Some')]
|
||||
public function testGetOrDefaultSucceedsWithSome(): void
|
||||
{
|
||||
$this->assertEquals('no', Option::Some('no')->getOrDefault('yes'), 'Value should have been from option');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrCall succeeds with None')]
|
||||
public function testGetOrCallSucceedsWithNone(): void
|
||||
{
|
||||
$value = Option::None()->getOrCall(new class { public function __invoke(): string { return 'called'; } });
|
||||
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
|
||||
}
|
||||
|
||||
#[TestDox('GetOrCall succeeds with Some')]
|
||||
public function testGetOrCallSucceedsWithSome(): void
|
||||
{
|
||||
$value = Option::Some('passed')->getOrCall(
|
||||
new class { public function __invoke(): string { return 'called'; } });
|
||||
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds with None')]
|
||||
public function testMapSucceedsWithNone(): void
|
||||
{
|
||||
$tattle = new class { public bool $called = false; };
|
||||
$none = Option::None();
|
||||
$mapped = $none->map(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return 'hello';
|
||||
});
|
||||
$this->assertTrue($mapped->isNone(), 'The mapped option should be "None"');
|
||||
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
|
||||
$this->assertSame($none, $mapped, 'The same "None" instance should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds with Some')]
|
||||
public function testMapSucceedsWithSome(): void
|
||||
{
|
||||
$mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2));
|
||||
$this->assertTrue($mapped->isSome(), 'The mapped option should be "Some"');
|
||||
$this->assertEquals('abc abc ', $mapped->get(), 'The mapping function was not called correctly');
|
||||
}
|
||||
|
||||
#[TestDox('Map fails with Some when mapping is null')]
|
||||
public function testMapFailsWithSomeWhenMappingIsNull(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Option::Some('oof')->map(fn($it) => null);
|
||||
}
|
||||
|
||||
#[TestDox('Iter succeeds with None')]
|
||||
public function testIterSucceedsWithNone(): void
|
||||
{
|
||||
$target = new class { public mixed $called = null; };
|
||||
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
|
||||
$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; };
|
||||
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
|
||||
$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();
|
||||
$filtered = $none->filter(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return true;
|
||||
});
|
||||
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"');
|
||||
$this->assertFalse($tattle->called, 'The callable should not have been called');
|
||||
$this->assertSame($none, $filtered, 'The "None" instance returned should have been the one passed');
|
||||
}
|
||||
|
||||
#[TestDox('Filter succeeds with Some when true')]
|
||||
public function testFilterSucceedsWithSomeWhenTrue(): void
|
||||
{
|
||||
$some = Option::Some(12);
|
||||
$filtered = $some->filter(fn($it) => $it % 2 === 0);
|
||||
$this->assertTrue($filtered->isSome(), 'The filtered option should have been "Some"');
|
||||
$this->assertEquals(12, $filtered->get(), 'The filtered option value is incorrect');
|
||||
$this->assertSame($some, $filtered, 'The same "Some" instance should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('Filter succeeds with Some when false')]
|
||||
public function testFilterSucceedsWithSomeWhenFalse(): void
|
||||
{
|
||||
$some = Option::Some(23);
|
||||
$filtered = $some->filter(fn($it) => $it % 2 === 0);
|
||||
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with None')]
|
||||
public function testIsSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertFalse(Option::None()->is(null), '"None" should always return false');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when strictly equal')]
|
||||
public function testIsSucceedsWithSomeWhenStrictlyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some(3)->is(3), '"Some" with strict equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when strictly unequal')]
|
||||
public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->is(3), '"Some" with strict equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when loosely equal')]
|
||||
public function testIsSucceedsWithSomeWhenLooselyEqual(): void
|
||||
{
|
||||
$this->assertTrue(Option::Some('3')->is(3, strict: false), '"Some" with loose equality should be true');
|
||||
}
|
||||
|
||||
#[TestDox('Is succeeds with Some when loosely unequal')]
|
||||
public function testIsSucceedsWithSomeWhenLooselyUnequal(): void
|
||||
{
|
||||
$this->assertFalse(Option::Some('3')->is(4, strict: false), '"Some" with loose equality should be false');
|
||||
}
|
||||
|
||||
#[TestDox('Unwrap succeeds with None')]
|
||||
public function testUnwrapSucceedsWithNone(): void
|
||||
{
|
||||
$this->assertNull(Option::None()->unwrap(), '"None" should return null');
|
||||
}
|
||||
|
||||
#[TestDox('Unwrap succeeds with Some')]
|
||||
public function testUnwrapSucceedsWithSome(): void
|
||||
{
|
||||
$this->assertEquals('boy howdy', Option::Some('boy howdy')->unwrap(), '"Some" should return its value');
|
||||
}
|
||||
|
||||
#[TestDox('Tap succeeds with Some')]
|
||||
public function testTapSucceedsWithSome(): void
|
||||
{
|
||||
$value = '';
|
||||
$original = Option::Some('testing');
|
||||
$tapped = $original->tap(
|
||||
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
|
||||
$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();
|
||||
$tapped = $original->tap(
|
||||
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
|
||||
$this->assertEquals('none', $value, 'The tapped function was not called');
|
||||
$this->assertSame($original, $tapped, 'The same option should have been returned');
|
||||
}
|
||||
|
||||
#[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');
|
||||
$this->assertTrue($it->isSome(), 'The option should have been "Some"');
|
||||
}
|
||||
|
||||
public function testSomeFailsWithNull(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Option::Some(null);
|
||||
}
|
||||
|
||||
public function testNoneSucceeds(): void
|
||||
{
|
||||
$it = Option::None();
|
||||
$this->assertTrue($it->isNone(), 'The option should have been "None"');
|
||||
}
|
||||
|
||||
public function testOfSucceedsWithNull(): void
|
||||
{
|
||||
$it = Option::of(null);
|
||||
$this->assertTrue($it->isNone(), '"null" should have created a "None" option');
|
||||
}
|
||||
|
||||
public function testOfSucceedsWithNonNull(): void
|
||||
{
|
||||
$it = Option::of('test');
|
||||
$this->assertTrue($it->isSome(), 'A non-null value should have created a "Some" option');
|
||||
$this->assertEquals('test', $it->get(), 'The value was not assigned correctly');
|
||||
}
|
||||
|
||||
#[TestDox('Of succeeds with PhpOption\Some')]
|
||||
public function testOfSucceedsWithPhpOptionSome(): void
|
||||
{
|
||||
$it = Option::of(Some::create('something'));
|
||||
$this->assertTrue($it->isSome(), 'A "Some" PhpOption should have created a "Some" option');
|
||||
$this->assertEquals('something', $it->get(), 'The value was not assigned correctly');
|
||||
}
|
||||
|
||||
#[TestDox('Of succeeds with PhpOption\None')]
|
||||
public function testOfSucceedsWithPhpOptionNone(): void
|
||||
{
|
||||
$it = Option::of(None::create());
|
||||
$this->assertTrue($it->isNone(), 'A "None" PhpOption should have created a "None" option');
|
||||
}
|
||||
}
|
||||
45
tests/Pest.php
Normal file
45
tests/Pest.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Test;
|
||||
|
||||
use BitBadger\InspiredByFSharp\Result;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for the Result class
|
||||
*/
|
||||
class ResultTest extends TestCase
|
||||
{
|
||||
#[TestDox('GetOK succeeds for OK result')]
|
||||
public function testGetOKSucceedsForOKResult(): void
|
||||
{
|
||||
$result = Result::OK('yay');
|
||||
$this->assertEquals('yay', $result->getOK(), 'The OK result should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('GetOK fails for Error result')]
|
||||
public function testGetOKFailsForErrorResult(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::Error('whoops')->getOK();
|
||||
}
|
||||
|
||||
#[TestDox('GetError succeeds for Error result')]
|
||||
public function testGetErrorSucceedsForErrorResult(): void
|
||||
{
|
||||
$result = Result::Error('boo');
|
||||
$this->assertEquals('boo', $result->getError(), 'The Error result should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('GetError fails for OK result')]
|
||||
public function testGetErrorFailsForOKResult(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::OK('yeah')->getError();
|
||||
}
|
||||
|
||||
#[TestDox('IsOK succeeds for OK result')]
|
||||
public function testIsOKSucceedsForOKResult(): void
|
||||
{
|
||||
$result = Result::OK('ok');
|
||||
$this->assertTrue($result->isOK(), 'The check for "OK" should have returned true');
|
||||
}
|
||||
|
||||
#[TestDox('IsOK succeeds for Error result')]
|
||||
public function testIsOKSucceedsForErrorResult(): void
|
||||
{
|
||||
$result = Result::Error('error');
|
||||
$this->assertFalse($result->isOK(), 'The check for "OK" should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('IsError succeeds for Error result')]
|
||||
public function testIsErrorSucceedsForErrorResult(): void
|
||||
{
|
||||
$result = Result::Error('not ok');
|
||||
$this->assertTrue($result->isError(), 'The check for "Error" should have returned true');
|
||||
}
|
||||
|
||||
#[TestDox('IsError succeeds for OK result')]
|
||||
public function testIsErrorSucceedsForOKResult(): void
|
||||
{
|
||||
$result = Result::OK('fine');
|
||||
$this->assertFalse($result->isError(), 'The check for "Error" should have returned false');
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds for OK result')]
|
||||
public function testMapSucceedsForOKResult(): void
|
||||
{
|
||||
$ok = Result::OK('yard');
|
||||
$mapped = $ok->map(fn($it) => strrev($it));
|
||||
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
|
||||
$this->assertEquals('dray', $mapped->getOK(), 'The mapping function was not called correctly');
|
||||
}
|
||||
|
||||
#[TestDox('Map fails for OK result when mapping is null')]
|
||||
public function testMapFailsForOKResultWhenMappingIsNull(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::OK('not null')->map(fn($it) => null);
|
||||
}
|
||||
|
||||
#[TestDox('Map succeeds for Error result')]
|
||||
public function testMapSucceedsForErrorResult(): void
|
||||
{
|
||||
$tattle = new class { public bool $called = false; };
|
||||
$error = Result::Error('nope');
|
||||
$mapped = $error->map(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return 'hello';
|
||||
});
|
||||
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
|
||||
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
|
||||
$this->assertSame($error, $mapped, 'The same "Error" instance should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('MapError succeeds for OK result')]
|
||||
public function testMapErrorSucceedsForOKResult(): void
|
||||
{
|
||||
$tattle = new class { public bool $called = false; };
|
||||
$ok = Result::OK('sure');
|
||||
$mapped = $ok->mapError(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return 'hello';
|
||||
});
|
||||
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
|
||||
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
|
||||
$this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('MapError succeeds for Error result')]
|
||||
public function testMapErrorSucceedsForErrorResult(): void
|
||||
{
|
||||
$error = Result::Error('taco');
|
||||
$mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it)));
|
||||
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
|
||||
$this->assertEquals('****', $mapped->getError(), 'The mapping function was not called correctly');
|
||||
}
|
||||
|
||||
#[TestDox('MapError fails for Error result when mapping is null')]
|
||||
public function testMapErrorFailsForErrorResultWhenMappingIsNull(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::Error('pizza')->mapError(fn($it) => null);
|
||||
}
|
||||
|
||||
#[TestDox('Iter succeeds for OK result')]
|
||||
public function testIterSucceedsForOKResult(): void
|
||||
{
|
||||
$target = new class { public mixed $called = null; };
|
||||
Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
|
||||
$this->assertEquals(77, $target->called, 'The function should have been called with the "OK" value');
|
||||
}
|
||||
|
||||
#[TestDox('Iter succeeds for Error result')]
|
||||
public function testIterSucceedsForErrorResult(): void
|
||||
{
|
||||
$target = new class { public mixed $called = null; };
|
||||
Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
|
||||
$this->assertNull($target->called, 'The function should not have been called');
|
||||
}
|
||||
|
||||
#[TestDox('ToOption succeeds for OK result')]
|
||||
public function testToOptionSucceedsForOKResult()
|
||||
{
|
||||
$value = Result::OK(99)->toOption();
|
||||
$this->assertTrue($value->isSome(), 'An "OK" result should map to a "Some" option');
|
||||
$this->assertEquals(99, $value->get(), 'The value is not correct');
|
||||
}
|
||||
|
||||
#[TestDox('ToOption succeeds for Error result')]
|
||||
public function testToOptionSucceedsForErrorResult()
|
||||
{
|
||||
$value = Result::Error('file not found')->toOption();
|
||||
$this->assertTrue($value->isNone(), 'An "Error" result should map to a "None" option');
|
||||
}
|
||||
|
||||
#[TestDox('Tap succeeds for OK result')]
|
||||
public function testTapSucceedsForOKResult(): void
|
||||
{
|
||||
$value = '';
|
||||
$original = Result::OK('working');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
});
|
||||
$this->assertEquals('OK: working', $value, 'The tapped function was not called');
|
||||
$this->assertSame($original, $tapped, 'The same result should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('Tap succeeds for Error result')]
|
||||
public function testTapSucceedsForErrorResult(): void
|
||||
{
|
||||
$value = '';
|
||||
$original = Result::Error('failed');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
});
|
||||
$this->assertEquals('Error: failed', $value, 'The tapped function was not called');
|
||||
$this->assertSame($original, $tapped, 'The same result should have been returned');
|
||||
}
|
||||
|
||||
#[TestDox('OK succeeds for non null result')]
|
||||
public function testOKSucceedsForNonNullResult(): void
|
||||
{
|
||||
$result = Result::OK('something');
|
||||
$this->assertTrue($result->isOK(), 'The result should have been "OK"');
|
||||
$this->assertEquals('something', $result->getOK(), 'The "OK" value was incorrect');
|
||||
}
|
||||
|
||||
#[TestDox('OK fails for null result')]
|
||||
public function testOKFailsForNullResult(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::OK(null);
|
||||
}
|
||||
|
||||
#[TestDox('Error succeeds for non null result')]
|
||||
public function testErrorSucceedsForNonNullResult(): void
|
||||
{
|
||||
$result = Result::Error('sad trombone');
|
||||
$this->assertTrue($result->isError(), 'The result should have been "Error"');
|
||||
$this->assertEquals('sad trombone', $result->getError(), 'The "Error" value was incorrect');
|
||||
}
|
||||
|
||||
#[TestDox('Error fails for null result')]
|
||||
public function testErrorFailsForNullResult(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
Result::Error(null);
|
||||
}
|
||||
}
|
||||
256
tests/Unit/OptionTest.php
Normal file
256
tests/Unit/OptionTest.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use PhpOption\{None, Some};
|
||||
|
||||
describe('->get()', function () {
|
||||
test('retrieves the value for Some', function () {
|
||||
expect(Option::Some(9)->get())->toBe(9);
|
||||
});
|
||||
test('throws an exception for None', function () {
|
||||
expect(fn() => Option::None()->get())->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()->get()->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()->get()->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()
|
||||
->get()->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->get() : '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->get() : '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()
|
||||
->get()->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()->get()->toBe('something');
|
||||
});
|
||||
});
|
||||
206
tests/Unit/ResultTest.php
Normal file
206
tests/Unit/ResultTest.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\{Option, Result};
|
||||
|
||||
describe('->getOK()', function () {
|
||||
test('returns OK value for OK result', function () {
|
||||
expect(Result::OK('yay')->getOK())->toBe('yay');
|
||||
});
|
||||
test('throws an exception for Error result', function () {
|
||||
expect(fn() => Result::Error('whoops')->getOK())->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->getError()', function () {
|
||||
test('throws an exception for OK result', function () {
|
||||
expect(fn() => Result::OK('yeah')->getError())->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
test('returns Error value for Error result', function () {
|
||||
expect(Result::Error('boo')->getError())->toBe('boo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isOK()', function () {
|
||||
test('returns true for OK result', function () {
|
||||
expect(Result::OK('ok')->isOK())->toBeTrue();
|
||||
});
|
||||
test('returns false for Error result', function () {
|
||||
expect(Result::Error('error')->isOK())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isError()', function () {
|
||||
test('returns false for OK result', function () {
|
||||
expect(Result::OK('fine')->isError())->toBeFalse();
|
||||
});
|
||||
test('returns true for Error result', function () {
|
||||
expect(Result::Error('not ok')->isError())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->bind()', function () {
|
||||
test('returns OK when binding against OK with OK', function () {
|
||||
expect(Result::OK('one')->bind(fn($it) => Result::OK("$it two")))
|
||||
->isOK()->toBeTrue()->getOK()->toBe('one two');
|
||||
});
|
||||
test('returns Error when binding against OK with Error', function () {
|
||||
expect(Result::OK('three')->bind(fn($it) => Result::Error('back to two')))
|
||||
->isError()->toBeTrue()->getError()->toBe('back to two');
|
||||
});
|
||||
test('returns Error when binding against Error', function () {
|
||||
$original = Result::Error('oops');
|
||||
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
|
||||
expect($result->isError())->toBeTrue()
|
||||
->and($result)->toBe($original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->contains()', function () {
|
||||
test('returns true when OK is strictly equal', function () {
|
||||
expect(Result::OK(18)->contains(18))->toBeTrue();
|
||||
});
|
||||
test('returns false when OK is not strictly equal', function () {
|
||||
expect(Result::OK(18)->contains('18'))->toBeFalse();
|
||||
});
|
||||
test('returns true when OK is loosely equal', function () {
|
||||
expect(Result::OK(18)->contains('18', strict: false))->toBeTrue();
|
||||
});
|
||||
test('returns false when OK is not loosely equal', function () {
|
||||
expect(Result::OK(18)->contains(17, strict: false))->toBeFalse();
|
||||
});
|
||||
test('returns false for Error', function () {
|
||||
expect(Result::Error('ouch')->contains('ouch'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->exists()', function () {
|
||||
test('returns true for OK when condition matches', function () {
|
||||
expect(Result::OK(14)->exists(fn($it) => $it < 100))->toBeTrue();
|
||||
});
|
||||
test('returns false for OK when condition does not match', function () {
|
||||
expect(Result::OK(14)->exists(fn($it) => $it > 100))->toBeFalse();
|
||||
});
|
||||
test('returns false for Error', function () {
|
||||
expect(Result::Error(true)->exists(fn($it) => true))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->map()', function () {
|
||||
test('maps value for OK', function () {
|
||||
expect(Result::OK('yard')->map(fn($it) => strrev($it)))
|
||||
->isOK()->toBeTrue()->getOK()->toBe('dray');
|
||||
});
|
||||
test('throws an exception for OK when mapping result is null', function () {
|
||||
expect(fn() => Result::OK('not null')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
test('does nothing for Error', function () {
|
||||
$tattle = new class { public bool $called = false; };
|
||||
$error = Result::Error('nope');
|
||||
$mapped = $error->map(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return 'hello';
|
||||
});
|
||||
expect($mapped)
|
||||
->isError()->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($mapped)->toBe($error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->mapError()', function () {
|
||||
test('does nothing for OK', function () {
|
||||
$tattle = new class { public bool $called = false; };
|
||||
$ok = Result::OK('sure');
|
||||
$mapped = $ok->mapError(function () use ($tattle)
|
||||
{
|
||||
$tattle->called = true;
|
||||
return 'hello';
|
||||
});
|
||||
expect($mapped)
|
||||
->isOK()->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($mapped)->toBe($ok);
|
||||
});
|
||||
test('maps value for Error', function () {
|
||||
expect(Result::Error('taco')->mapError(fn($it) => str_repeat('*', strlen($it))))
|
||||
->isError()->toBeTrue()->getError()->toBe('****');
|
||||
});
|
||||
test('throws an exception for Error when mapping result is null', function () {
|
||||
expect(fn() => Result::Error('pizza')->mapError(fn($it) => null))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->iter()', function () {
|
||||
test('iterates for OK', function () {
|
||||
$target = new class { public mixed $called = null; };
|
||||
Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
|
||||
expect($target->called)->toBe(77);
|
||||
});
|
||||
test('does nothing for Error', function () {
|
||||
$target = new class { public mixed $called = null; };
|
||||
Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
|
||||
expect($target->called)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->toArray()', function () {
|
||||
test('returns a one-item array for OK', function () {
|
||||
expect(Result::OK('yay')->toArray())->not->toBeNull()->toBe(['yay']);
|
||||
});
|
||||
test('returns an empty array for Error', function () {
|
||||
expect(Result::Error('oh no')->toArray())->not->toBeNull()->toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->toOption()', function () {
|
||||
test('returns a Some option for OK', function () {
|
||||
expect(Result::OK(99)->toOption())->isSome()->toBeTrue()->get()->toBe(99);
|
||||
});
|
||||
test('returns a None option for Error', function () {
|
||||
expect(Result::Error('file not found')->toOption())->isNone()->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->tap()', function () {
|
||||
test('is called for OK', function () {
|
||||
$value = '';
|
||||
$original = Result::OK('working');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
});
|
||||
expect($value)->toBe('OK: working')->and($tapped)->toBe($original);
|
||||
});
|
||||
test('is called for Error', function () {
|
||||
$value = '';
|
||||
$original = Result::Error('failed');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
});
|
||||
expect($value)->toBe('Error: failed')->and($tapped)->toBe($original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::OK()', function () {
|
||||
test('creates an OK result for a non-null value', function () {
|
||||
expect(Result::OK('something'))->isOK()->toBeTrue()->getOK()->toBe('something');
|
||||
});
|
||||
test('throws an exception for OK with a null value', function () {
|
||||
expect(fn() => Result::OK(null))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::Error()', function () {
|
||||
test('creates an Error result for a non-null value', function () {
|
||||
expect(Result::Error('sad trombone'))->isError()->toBeTrue()->getError()->toBe('sad trombone');
|
||||
});
|
||||
test('throws an exception for Error with a null value', function () {
|
||||
expect(fn() => Result::Error(null))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user