5 Commits

Author SHA1 Message Date
9327d8fa29 Update properties in README 2024-09-30 23:06:57 -04:00
483d7875d5 Change functions to properties
- Force PHP 8.4
2024-09-30 22:59:46 -04:00
6779b2c554 Restrict PHP version
- Update deps
2024-09-30 20:14:52 -04:00
fad428a4e4 Add contains, exists, toArray
- Update docs
2024-07-29 13:58:33 -04:00
57af645d87 Add bind() to option and result 2024-07-28 22:50:59 -04:00
8 changed files with 491 additions and 254 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea .idea
vendor vendor
*.tests.txt

View File

@@ -2,29 +2,38 @@
This project contains PHP utility classes whose functionality is inspired by their F# counterparts. This project contains PHP utility classes whose functionality is inspired by their F# counterparts.
The v2 series requires at least PHP 8.4. A similar API exists for PHP 8.2 - 8.3 in version 1 of this project; see its README for specifics.
## What It Provides ## What It Provides
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. 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 | | | `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 | | **Creating** | `::Some(T)` for Some | `::OK(TOK)` for OK |
| **Querying** | `->isSome()`<br>`->isNone()` | `->isOK()`<br>`->isError()` | | | `::None()` for None | `::Error(TError)` for Error |
| **Reading**<br>_throws if called on missing value_ | `->get()` | `->getOK()`<br>`->getError()` | | | `::of($value)` _None if `null`_ | |
| **Transforming**<br>_still `Option` or `Result`_ | `->map(callable(T): U)` | `->map(callable(TOK): U)`<br>`->mapError(callable(TError): U)` | | **Querying** | `->isSome: bool` | `->isOK: bool` |
| **Iterating** | `->iter(callable(T): void)` | `->iter(callable(TOK): void)` | | | `->isNone: bool` | `->isError: bool` |
| **Inspecting**<br>_returns the original instance_ | `->tap(callable(Option<T>): void)` | `->tap(callable(Result<TOK, TError>): void)` | | | `->contains(T, $strict = true): bool` | `->contains(TOK, $strict = true): bool` |
| | `->exists(callable(T): bool): bool` | `->exists(callable(TOK): bool): bool` |
| **Reading**<br> | `->value: T` | `->ok: TOK` |
| _all throw if called on missing value_ | | `->error: 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: 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. - `->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`. - `->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. - `->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. - `->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. 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 ## The Inspiration

View File

@@ -17,7 +17,7 @@
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss" "rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss"
}, },
"require": { "require": {
"php": "^8.2" "php": ">=8.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11", "phpunit/phpunit": "^11",

114
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9c9d70d95d369f37fa95a10637a56a58", "content-hash": "0cd12c9ee6159a96158d62691add3401",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@@ -69,16 +69,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.1.0", "version": "v5.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -121,9 +121,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
}, },
"time": "2024-07-01T20:03:41+00:00" "time": "2024-09-29T13:56:26+00:00"
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
@@ -320,32 +320,32 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "11.0.5", "version": "11.0.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
"reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^5.0", "nikic/php-parser": "^5.1.0",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-file-iterator": "^5.0", "phpunit/php-file-iterator": "^5.0.1",
"phpunit/php-text-template": "^4.0", "phpunit/php-text-template": "^4.0.1",
"sebastian/code-unit-reverse-lookup": "^4.0", "sebastian/code-unit-reverse-lookup": "^4.0.1",
"sebastian/complexity": "^4.0", "sebastian/complexity": "^4.0.1",
"sebastian/environment": "^7.0", "sebastian/environment": "^7.2.0",
"sebastian/lines-of-code": "^3.0", "sebastian/lines-of-code": "^3.0.1",
"sebastian/version": "^5.0", "sebastian/version": "^5.0.1",
"theseer/tokenizer": "^1.2.0" "theseer/tokenizer": "^1.2.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.0"
@@ -357,7 +357,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "11.0-dev" "dev-main": "11.0.x-dev"
} }
}, },
"autoload": { "autoload": {
@@ -386,7 +386,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.5" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6"
}, },
"funding": [ "funding": [
{ {
@@ -394,20 +394,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-07-03T05:05:37+00:00" "time": "2024-08-22T04:37:56+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
"version": "5.0.1", "version": "5.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
"reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -447,7 +447,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1" "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0"
}, },
"funding": [ "funding": [
{ {
@@ -455,7 +455,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-07-03T05:06:37+00:00" "time": "2024-08-27T05:02:59+00:00"
}, },
{ {
"name": "phpunit/php-invoker", "name": "phpunit/php-invoker",
@@ -643,16 +643,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.2.8", "version": "11.3.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39" "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a7a29e8d3113806f18f99d670f580a30e8ffff39", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"reference": "a7a29e8d3113806f18f99d670f580a30e8ffff39", "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -666,20 +666,20 @@
"phar-io/manifest": "^2.0.4", "phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.5", "phpunit/php-code-coverage": "^11.0.6",
"phpunit/php-file-iterator": "^5.0.1", "phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1", "phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1", "phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2", "sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.1", "sebastian/code-unit": "^3.0.1",
"sebastian/comparator": "^6.0.1", "sebastian/comparator": "^6.1.0",
"sebastian/diff": "^6.0.2", "sebastian/diff": "^6.0.2",
"sebastian/environment": "^7.2.0", "sebastian/environment": "^7.2.0",
"sebastian/exporter": "^6.1.3", "sebastian/exporter": "^6.1.3",
"sebastian/global-state": "^7.0.2", "sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1", "sebastian/object-enumerator": "^6.0.1",
"sebastian/type": "^5.0.1", "sebastian/type": "^5.1.0",
"sebastian/version": "^5.0.1" "sebastian/version": "^5.0.1"
}, },
"suggest": { "suggest": {
@@ -691,7 +691,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "11.2-dev" "dev-main": "11.3-dev"
} }
}, },
"autoload": { "autoload": {
@@ -723,7 +723,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.2.8" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
}, },
"funding": [ "funding": [
{ {
@@ -739,7 +739,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-07-18T14:56:37+00:00" "time": "2024-09-19T10:54:28+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
@@ -913,16 +913,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "6.0.1", "version": "6.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "131942b86d3587291067a94f295498ab6ac79c20" "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/131942b86d3587291067a94f295498ab6ac79c20", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"reference": "131942b86d3587291067a94f295498ab6ac79c20", "reference": "fa37b9e2ca618cb051d71b60120952ee8ca8b03d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -933,12 +933,12 @@
"sebastian/exporter": "^6.0" "sebastian/exporter": "^6.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "6.0-dev" "dev-main": "6.1-dev"
} }
}, },
"autoload": { "autoload": {
@@ -978,7 +978,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/6.0.1" "source": "https://github.com/sebastianbergmann/comparator/tree/6.1.0"
}, },
"funding": [ "funding": [
{ {
@@ -986,7 +986,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-07-03T04:48:07+00:00" "time": "2024-09-11T15:42:56+00:00"
}, },
{ {
"name": "sebastian/complexity", "name": "sebastian/complexity",
@@ -1555,28 +1555,28 @@
}, },
{ {
"name": "sebastian/type", "name": "sebastian/type",
"version": "5.0.1", "version": "5.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/type.git", "url": "https://github.com/sebastianbergmann/type.git",
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac",
"reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.2" "php": ">=8.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^11.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "5.0-dev" "dev-main": "5.1-dev"
} }
}, },
"autoload": { "autoload": {
@@ -1600,7 +1600,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/type/issues", "issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy", "security": "https://github.com/sebastianbergmann/type/security/policy",
"source": "https://github.com/sebastianbergmann/type/tree/5.0.1" "source": "https://github.com/sebastianbergmann/type/tree/5.1.0"
}, },
"funding": [ "funding": [
{ {
@@ -1608,7 +1608,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-07-03T05:11:49+00:00" "time": "2024-09-17T13:12:04+00:00"
}, },
{ {
"name": "sebastian/version", "name": "sebastian/version",
@@ -1721,7 +1721,7 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8" "php": ">=8.4"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.6.0" "plugin-api-version": "2.6.0"

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace BitBadger\InspiredByFSharp; namespace BitBadger\InspiredByFSharp;
use Error; use Error;
use Exception;
use InvalidArgumentException; use InvalidArgumentException;
/** /**
@@ -24,50 +25,38 @@ use InvalidArgumentException;
* *
* @template T The type of value represented by this option * @template T The type of value represented by this option
*/ */
readonly class Option class Option
{ {
/** @var T|null $value The value for this option */ /** @var T|null $value The value for this option */
private mixed $value; private mixed $val;
/** /**
* @param T|null $value The possibly null value for this option * @param T|null $value The possibly null value for this option
*/ */
private function __construct(mixed $value = null) private function __construct(mixed $value = null)
{ {
$this->value = $value; $this->val = $value;
} }
/** /**
* Get the value of this option * @var T The value of this option (read-only)
* * @throws InvalidArgumentException If called on a `None` option
* @return T The value of the option
*/ */
public function get(): mixed public mixed $value {
{ get => match ($this->val) {
return match (true) { null => throw new InvalidArgumentException('Cannot get the value of a None option'),
$this->isSome() => $this->value, default => $this->val,
default => throw new InvalidArgumentException('Cannot get the value of a None option'),
}; };
} }
/** /** @var bool True if the option is `None`, false if it is `Some` */
* Does this option have a `None` value? public bool $isNone {
* get => is_null($this->val);
* @return bool True if the option is `None`, false if it is `Some`
*/
public function isNone(): bool
{
return is_null($this->value);
} }
/** /** @var bool True if the option is `Some`, false if it is `None` */
* Does this option have a `Some` value? public bool $isSome{
* get => !$this->isNone;
* @return bool True if the option is `Some`, false if it is `None`
*/
public function isSome(): bool
{
return !$this->isNone();
} }
/** /**
@@ -78,31 +67,84 @@ readonly class Option
*/ */
public function getOrDefault(mixed $default): mixed public function getOrDefault(mixed $default): mixed
{ {
return $this->value ?? $default; return $this->val ?? $default;
} }
/** /**
* Get the value, or return the value of a callable function * Get the value, or return the value of a callable function
* *
* @template U The return type of the callable provided * @template TCalled The return type of the callable provided
* @param callable(): U $f The callable function to use for `None` options * @param callable(): TCalled $f The callable function to use for `None` options
* @return T|mixed The value if `Some`, the result of the callable if `None` * @return T|TCalled The value if `Some`, the result of the callable if `None`
*/ */
public function getOrCall(callable $f): mixed public function getOrCall(callable $f): mixed
{ {
return $this->value ?? $f(); return $this->val ?? $f();
}
/**
* Get the value, or throw the exception using the given function
*
* @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->val ?? 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->val);
}
/**
* 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->val === $value : $this->val == $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->val) : false;
} }
/** /**
* Map this optional value to another value * Map this optional value to another value
* *
* @template U The type of the mapping function * @template TMapped The type of the mapping function
* @param callable(T): U $f The mapping function * @param callable(T): TMapped $f The mapping function
* @return Option<U> A `Some` instance with the transformed value if `Some`, `None` otherwise * @return Option<TMapped> A `Some` instance with the transformed value if `Some`, `None` otherwise
*/ */
public function map(callable $f): self public function map(callable $f): self
{ {
return $this->isSome() ? self::Some($f($this->get())) : $this; return $this->isSome ? self::Some($f($this->val)) : $this;
} }
/** /**
@@ -112,8 +154,8 @@ readonly class Option
*/ */
public function iter(callable $f): void public function iter(callable $f): void
{ {
if ($this->isSome()) { if ($this->isSome) {
$f($this->value); $f($this->val);
} }
} }
@@ -125,25 +167,7 @@ readonly class Option
*/ */
public function filter(callable $f): self public function filter(callable $f): self
{ {
return match (true) { return $this->isNone || $this->exists($f) ? $this : self::None();
$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,
};
} }
/** /**
@@ -153,7 +177,7 @@ readonly class Option
*/ */
public function unwrap(): mixed public function unwrap(): mixed
{ {
return $this->value; return $this->val;
} }
/** /**
@@ -168,6 +192,16 @@ readonly class Option
return $this; 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->val] : [];
}
/** /**
* Convert this to a PhpOption option * Convert this to a PhpOption option
* *
@@ -176,8 +210,8 @@ readonly class Option
public function toPhpOption(): mixed public function toPhpOption(): mixed
{ {
return match (true) { return match (true) {
$this->isNone() && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'), $this->isNone && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'),
class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->value), class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->val),
default => throw new Error('PhpOption types could not be found'), default => throw new Error('PhpOption types could not be found'),
}; };
} }
@@ -216,7 +250,7 @@ readonly class Option
{ {
return match (true) { return match (true) {
is_object($value) && is_a($value, 'PhpOption\Option') => is_object($value) && is_a($value, 'PhpOption\Option') =>
$value->isDefined() ? self::Some($value->get()) : self::None(), $value->isDefined() ? self::Some($value->get()) : self::None(),
default => new self($value), default => new self($value),
}; };
} }

View File

@@ -25,7 +25,7 @@ use InvalidArgumentException;
* @template TOK The type of the OK result * @template TOK The type of the OK result
* @template TError The type of the error result * @template TError The type of the error result
*/ */
readonly class Result class Result
{ {
/** @var Option<TOK> The OK value for this result */ /** @var Option<TOK> The OK value for this result */
private Option $okValue; private Option $okValue;
@@ -45,70 +45,90 @@ readonly class Result
$this->errorValue = Option::of($errorValue); $this->errorValue = Option::of($errorValue);
} }
/** /** @var TOK The OK value (will throw if result is not OK) */
* Get the value for an `OK` result public mixed $ok {
* get => $this->okValue->value;
* @return TOK The OK value for this result }
* @throws InvalidArgumentException If the result is an `Error` result
*/ /** @var TError The error value (will throw if result is not Error) */
public function getOK(): mixed public mixed $error {
{ get => $this->errorValue->value;
return $this->okValue->get(); }
/** @var bool True if the result is `OK`, false if it is `Error` */
public bool $isOK {
get => $this->okValue->isSome;
}
/** @var bool True if the result is `Error`, false if it is `OK` */
public bool $isError {
get => $this->errorValue->isSome;
} }
/** /**
* Get the value for an `Error` result * Bind a function to this result (railway processing)
* *
* @return TError The error value for this result * If this result is OK, the function will be called with the OK value of the result. If this result is Error, it
* @throws InvalidArgumentException If the result is an `OK` result * 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 getError(): mixed public function bind(callable $f): Result
{ {
return $this->errorValue->get(); return $this->isError ? $this : $f($this->ok);
} }
/** /**
* Is this result `OK`? * Does this result's "OK" value match the given value?
* *
* @return bool True if the result is `OK`, false if it is `Error` * @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 isOK(): bool public function contains(mixed $value, bool $strict = true): bool
{ {
return $this->okValue->isSome(); return match (true) {
$this->isError => false,
default => $this->okValue->contains($value, $strict),
};
} }
/** /**
* Is this result `Error`? * Does the "OK" value of this result match the given predicate function?
* *
* @return bool True if the result is `Error`, false if it is `OK` * @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 isError(): bool public function exists(callable $f): bool
{ {
return $this->errorValue->isSome(); return $this->isOK ? $f($this->ok) : false;
} }
/** /**
* Map an `OK` result to another, leaving an `Error` result unmodified * Map an `OK` result to another, leaving an `Error` result unmodified
* *
* @template U The type of the mapping function * @template TMapped The type of the mapping function
* @param callable(TOK): U $f The mapping function * @param callable(TOK): TMapped $f The mapping function
* @return Result<U, TError> A transformed `OK` instance or the original `Error` instance * @return Result<TMapped, TError> A transformed `OK` instance or the original `Error` instance
*/ */
public function map(callable $f): self public function map(callable $f): self
{ {
return $this->isOK() ? self::OK($f($this->getOK())) : $this; return $this->isOK ? self::OK($f($this->ok)) : $this;
} }
/** /**
* Map an `Error` result to another, leaving an `OK` result unmodified * Map an `Error` result to another, leaving an `OK` result unmodified
* *
* @template U The type of the mapping function * @template TMapped The type of the mapping function
* @param callable(TError): U $f The mapping function * @param callable(TError): TMapped $f The mapping function
* @return Result<TOK, U> A transformed `Error` instance or the original `OK` instance * @return Result<TOK, TMapped> A transformed `Error` instance or the original `OK` instance
*/ */
public function mapError(callable $f): self public function mapError(callable $f): self
{ {
return $this->isError() ? self::Error($f($this->getError())) : $this; return $this->isError ? self::Error($f($this->error)) : $this;
} }
/** /**
@@ -118,11 +138,21 @@ readonly class Result
*/ */
public function iter(callable $f): void public function iter(callable $f): void
{ {
if ($this->isOK()) { if ($this->isOK) {
$f($this->getOK()); $f($this->ok);
} }
} }
/**
* 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` * Transform a `Result`'s `OK` value to an `Option`
* *
@@ -130,7 +160,7 @@ readonly class Result
*/ */
public function toOption(): Option public function toOption(): Option
{ {
return $this->isOK() ? Option::Some($this->getOK()) : Option::None(); return $this->isOK ? Option::Some($this->ok) : Option::None();
} }
/** /**

View File

@@ -8,6 +8,7 @@ declare(strict_types=1);
namespace Test; namespace Test;
use BadMethodCallException;
use BitBadger\InspiredByFSharp\Option; use BitBadger\InspiredByFSharp\Option;
use InvalidArgumentException; use InvalidArgumentException;
use PhpOption\{None, Some}; use PhpOption\{None, Some};
@@ -19,43 +20,43 @@ use PHPUnit\Framework\TestCase;
*/ */
class OptionTest extends TestCase class OptionTest extends TestCase
{ {
#[TestDox('Get succeeds for Some')] #[TestDox('Value succeeds for Some')]
public function testGetSucceedsForSome(): void public function testValueSucceedsForSome(): void
{ {
$it = Option::Some(9); $it = Option::Some(9);
$this->assertTrue($it->isSome(), 'The option should have been "Some"'); $this->assertTrue($it->isSome, 'The option should have been "Some"');
$this->assertEquals(9, $it->get(), 'The value was incorrect'); $this->assertEquals(9, $it->value, 'The value was incorrect');
} }
#[TestDox('Get fails for None')] #[TestDox('Value fails for None')]
public function testGetFailsForNone(): void public function testValueFailsForNone(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
Option::None()->get(); Option::None()->value;
} }
#[TestDox('IsNone succeeds with None')] #[TestDox('IsNone succeeds with None')]
public function testIsNoneSucceedsWithNone(): void public function testIsNoneSucceedsWithNone(): void
{ {
$this->assertTrue(Option::None()->isNone(), '"None" should return true'); $this->assertTrue(Option::None()->isNone, '"None" should return true');
} }
#[TestDox('IsNone succeeds with Some')] #[TestDox('IsNone succeeds with Some')]
public function testIsNoneSucceedsWithSome(): void public function testIsNoneSucceedsWithSome(): void
{ {
$this->assertFalse(Option::Some(8)->isNone(), '"Some" should return false'); $this->assertFalse(Option::Some(8)->isNone, '"Some" should return false');
} }
#[TestDox('IsSome succeeds with None')] #[TestDox('IsSome succeeds with None')]
public function testIsSomeSucceedsWithNone(): void public function testIsSomeSucceedsWithNone(): void
{ {
$this->assertFalse(Option::None()->isSome(), '"None" should return false'); $this->assertFalse(Option::None()->isSome, '"None" should return false');
} }
#[TestDox('IsSome succeeds with Some')] #[TestDox('IsSome succeeds with Some')]
public function testIsSomeSucceedsWithSome(): void public function testIsSomeSucceedsWithSome(): void
{ {
$this->assertTrue(Option::Some('boo')->isSome(), '"Some" should return true'); $this->assertTrue(Option::Some('boo')->isSome, '"Some" should return true');
} }
#[TestDox('GetOrDefault succeeds with None')] #[TestDox('GetOrDefault succeeds with None')]
@@ -85,6 +86,92 @@ class OptionTest extends TestCase
$this->assertEquals('passed', $value, 'The value should have been obtained from the option'); $this->assertEquals('passed', $value, 'The value should have been obtained from the option');
} }
#[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'));
}
#[TestDox('Bind succeeds with None')]
public function testBindSucceedsWithNone(): void
{
$original = Option::None();
$bound = $original->bind(fn($it) => Option::Some('value'));
$this->assertTrue($bound->isNone, 'The option should have been None');
$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'));
$this->assertTrue($bound->isSome, 'The option should have been Some');
$this->assertEquals('goodbye', $bound->value, 'The bound function was not called');
}
#[TestDox('Bind succeeds with Some and None')]
public function testBindSucceedsWithSomeAndNone(): void
{
$bound = Option::Some('greetings')->bind(fn($it) => Option::None());
$this->assertTrue($bound->isNone, 'The option should have been None');
}
#[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');
}
#[TestDox('Map succeeds with None')] #[TestDox('Map succeeds with None')]
public function testMapSucceedsWithNone(): void public function testMapSucceedsWithNone(): void
{ {
@@ -95,7 +182,7 @@ class OptionTest extends TestCase
$tattle->called = true; $tattle->called = true;
return 'hello'; return 'hello';
}); });
$this->assertTrue($mapped->isNone(), 'The mapped option should be "None"'); $this->assertTrue($mapped->isNone, 'The mapped option should be "None"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called'); $this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($none, $mapped, 'The same "None" instance should have been returned'); $this->assertSame($none, $mapped, 'The same "None" instance should have been returned');
} }
@@ -104,8 +191,8 @@ class OptionTest extends TestCase
public function testMapSucceedsWithSome(): void public function testMapSucceedsWithSome(): void
{ {
$mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2)); $mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2));
$this->assertTrue($mapped->isSome(), 'The mapped option should be "Some"'); $this->assertTrue($mapped->isSome, 'The mapped option should be "Some"');
$this->assertEquals('abc abc ', $mapped->get(), 'The mapping function was not called correctly'); $this->assertEquals('abc abc ', $mapped->value, 'The mapping function was not called correctly');
} }
#[TestDox('Map fails with Some when mapping is null')] #[TestDox('Map fails with Some when mapping is null')]
@@ -141,7 +228,7 @@ class OptionTest extends TestCase
$tattle->called = true; $tattle->called = true;
return true; return true;
}); });
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"'); $this->assertTrue($filtered->isNone, 'The filtered option should have been "None"');
$this->assertFalse($tattle->called, 'The callable should not have been called'); $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'); $this->assertSame($none, $filtered, 'The "None" instance returned should have been the one passed');
} }
@@ -151,8 +238,8 @@ class OptionTest extends TestCase
{ {
$some = Option::Some(12); $some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0); $filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isSome(), 'The filtered option should have been "Some"'); $this->assertTrue($filtered->isSome, 'The filtered option should have been "Some"');
$this->assertEquals(12, $filtered->get(), 'The filtered option value is incorrect'); $this->assertEquals(12, $filtered->value, 'The filtered option value is incorrect');
$this->assertSame($some, $filtered, 'The same "Some" instance should have been returned'); $this->assertSame($some, $filtered, 'The same "Some" instance should have been returned');
} }
@@ -161,37 +248,7 @@ class OptionTest extends TestCase
{ {
$some = Option::Some(23); $some = Option::Some(23);
$filtered = $some->filter(fn($it) => $it % 2 === 0); $filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"'); $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')] #[TestDox('Unwrap succeeds with None')]
@@ -212,7 +269,7 @@ class OptionTest extends TestCase
$value = ''; $value = '';
$original = Option::Some('testing'); $original = Option::Some('testing');
$tapped = $original->tap( $tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; }); function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
$this->assertEquals('testing', $value, 'The tapped function was not called'); $this->assertEquals('testing', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned'); $this->assertSame($original, $tapped, 'The same option should have been returned');
} }
@@ -223,11 +280,27 @@ class OptionTest extends TestCase
$value = ''; $value = '';
$original = Option::None(); $original = Option::None();
$tapped = $original->tap( $tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; }); function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
$this->assertEquals('none', $value, 'The tapped function was not called'); $this->assertEquals('none', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned'); $this->assertSame($original, $tapped, 'The same option should have been returned');
} }
#[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');
}
#[TestDox('ToPhpOption succeeds for Some')] #[TestDox('ToPhpOption succeeds for Some')]
public function testToPhpOptionSucceedsForSome(): void public function testToPhpOptionSucceedsForSome(): void
{ {
@@ -250,7 +323,7 @@ class OptionTest extends TestCase
public function testSomeSucceedsWithValue(): void public function testSomeSucceedsWithValue(): void
{ {
$it = Option::Some('hello'); $it = Option::Some('hello');
$this->assertTrue($it->isSome(), 'The option should have been "Some"'); $this->assertTrue($it->isSome, 'The option should have been "Some"');
} }
public function testSomeFailsWithNull(): void public function testSomeFailsWithNull(): void
@@ -262,34 +335,34 @@ class OptionTest extends TestCase
public function testNoneSucceeds(): void public function testNoneSucceeds(): void
{ {
$it = Option::None(); $it = Option::None();
$this->assertTrue($it->isNone(), 'The option should have been "None"'); $this->assertTrue($it->isNone, 'The option should have been "None"');
} }
public function testOfSucceedsWithNull(): void public function testOfSucceedsWithNull(): void
{ {
$it = Option::of(null); $it = Option::of(null);
$this->assertTrue($it->isNone(), '"null" should have created a "None" option'); $this->assertTrue($it->isNone, '"null" should have created a "None" option');
} }
public function testOfSucceedsWithNonNull(): void public function testOfSucceedsWithNonNull(): void
{ {
$it = Option::of('test'); $it = Option::of('test');
$this->assertTrue($it->isSome(), 'A non-null value should have created a "Some" option'); $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'); $this->assertEquals('test', $it->value, 'The value was not assigned correctly');
} }
#[TestDox('Of succeeds with PhpOption\Some')] #[TestDox('Of succeeds with PhpOption\Some')]
public function testOfSucceedsWithPhpOptionSome(): void public function testOfSucceedsWithPhpOptionSome(): void
{ {
$it = Option::of(Some::create('something')); $it = Option::of(Some::create('something'));
$this->assertTrue($it->isSome(), 'A "Some" PhpOption should have created a "Some" option'); $this->assertTrue($it->isSome, 'A "Some" PhpOption should have created a "Some" option');
$this->assertEquals('something', $it->get(), 'The value was not assigned correctly'); $this->assertEquals('something', $it->value, 'The value was not assigned correctly');
} }
#[TestDox('Of succeeds with PhpOption\None')] #[TestDox('Of succeeds with PhpOption\None')]
public function testOfSucceedsWithPhpOptionNone(): void public function testOfSucceedsWithPhpOptionNone(): void
{ {
$it = Option::of(None::create()); $it = Option::of(None::create());
$this->assertTrue($it->isNone(), 'A "None" PhpOption should have created a "None" option'); $this->assertTrue($it->isNone, 'A "None" PhpOption should have created a "None" option');
} }
} }

View File

@@ -18,60 +18,134 @@ use PHPUnit\Framework\TestCase;
*/ */
class ResultTest extends TestCase class ResultTest extends TestCase
{ {
#[TestDox('GetOK succeeds for OK result')] #[TestDox('OK property succeeds for OK result')]
public function testGetOKSucceedsForOKResult(): void public function testOKPropertySucceedsForOKResult(): void
{ {
$result = Result::OK('yay'); $result = Result::OK('yay');
$this->assertEquals('yay', $result->getOK(), 'The OK result should have been returned'); $this->assertEquals('yay', $result->ok, 'The OK result should have been returned');
} }
#[TestDox('GetOK fails for Error result')] #[TestDox('OK property fails for Error result')]
public function testGetOKFailsForErrorResult(): void public function testOKPropertyFailsForErrorResult(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
Result::Error('whoops')->getOK(); Result::Error('whoops')->ok;
} }
#[TestDox('GetError succeeds for Error result')] #[TestDox('Error property succeeds for Error result')]
public function testGetErrorSucceedsForErrorResult(): void public function testErrorPropertySucceedsForErrorResult(): void
{ {
$result = Result::Error('boo'); $result = Result::Error('boo');
$this->assertEquals('boo', $result->getError(), 'The Error result should have been returned'); $this->assertEquals('boo', $result->error, 'The Error result should have been returned');
} }
#[TestDox('GetError fails for OK result')] #[TestDox('Error property fails for OK result')]
public function testGetErrorFailsForOKResult(): void public function testErrorPropertyFailsForOKResult(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
Result::OK('yeah')->getError(); Result::OK('yeah')->error;
} }
#[TestDox('IsOK succeeds for OK result')] #[TestDox('IsOK succeeds for OK result')]
public function testIsOKSucceedsForOKResult(): void public function testIsOKSucceedsForOKResult(): void
{ {
$result = Result::OK('ok'); $result = Result::OK('ok');
$this->assertTrue($result->isOK(), 'The check for "OK" should have returned true'); $this->assertTrue($result->isOK, 'The check for "OK" should have returned true');
} }
#[TestDox('IsOK succeeds for Error result')] #[TestDox('IsOK succeeds for Error result')]
public function testIsOKSucceedsForErrorResult(): void public function testIsOKSucceedsForErrorResult(): void
{ {
$result = Result::Error('error'); $result = Result::Error('error');
$this->assertFalse($result->isOK(), 'The check for "OK" should have returned false'); $this->assertFalse($result->isOK, 'The check for "OK" should have returned false');
} }
#[TestDox('IsError succeeds for Error result')] #[TestDox('IsError succeeds for Error result')]
public function testIsErrorSucceedsForErrorResult(): void public function testIsErrorSucceedsForErrorResult(): void
{ {
$result = Result::Error('not ok'); $result = Result::Error('not ok');
$this->assertTrue($result->isError(), 'The check for "Error" should have returned true'); $this->assertTrue($result->isError, 'The check for "Error" should have returned true');
} }
#[TestDox('IsError succeeds for OK result')] #[TestDox('IsError succeeds for OK result')]
public function testIsErrorSucceedsForOKResult(): void public function testIsErrorSucceedsForOKResult(): void
{ {
$result = Result::OK('fine'); $result = Result::OK('fine');
$this->assertFalse($result->isError(), 'The check for "Error" should have returned false'); $this->assertFalse($result->isError, 'The check for "Error" should have returned false');
}
#[TestDox('Bind succeeds for OK with OK')]
public function testBindSucceedsForOKWithOK(): void
{
$result = Result::OK('one')->bind(fn($it) => Result::OK("$it two"));
$this->assertTrue($result->isOK, 'The result should have been OK');
$this->assertEquals('one two', $result->ok, 'The bound function was not called');
}
#[TestDox('Bind succeeds for OK with Error')]
public function testBindSucceedsForOKWithError(): void
{
$result = Result::OK('three')->bind(fn($it) => Result::Error('back to two'));
$this->assertTrue($result->isError, 'The result should have been Error');
$this->assertEquals('back to two', $result->error, 'The bound function was not called');
}
#[TestDox('Bind succeeds for Error')]
public function testBindSucceedsForError(): void
{
$original = Result::Error('oops');
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
$this->assertTrue($result->isError, 'The result should have been Error');
$this->assertSame($original, $result, 'The same Error result should have been returned');
}
#[TestDox('Contains succeeds for Error result')]
public function testContainsSucceedsForErrorResult(): void
{
$this->assertFalse(Result::Error('ouch')->contains('ouch'), '"Error" should always return false');
}
#[TestDox('Contains succeeds for OK result when strictly equal')]
public function testContainsSucceedsForOKResultWhenStrictlyEqual(): void
{
$this->assertTrue(Result::OK(18)->contains(18), '"OK" with strict equality should be true');
}
#[TestDox('Contains succeeds for OK result when strictly unequal')]
public function testContainsSucceedsForOKResultWhenStrictlyUnequal(): void
{
$this->assertFalse(Result::OK(18)->contains('18'), '"OK" with strict equality should be false');
}
#[TestDox('Contains succeeds for OK result when loosely equal')]
public function testContainsSucceedsForOKResultWhenLooselyEqual(): void
{
$this->assertTrue(Result::OK(18)->contains('18', strict: false), '"OK" with loose equality should be true');
}
#[TestDox('Contains succeeds for OK result when loosely unequal')]
public function testContainsSucceedsForOKResultWhenLooselyUnequal(): void
{
$this->assertFalse(Result::OK(18)->contains(17, strict: false), '"OK" with loose equality should be false');
}
#[TestDox('Exists succeeds for OK result when matching')]
public function testExistsSucceedsForOKResultWhenMatching(): void
{
$this->assertTrue(Result::OK(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
}
#[TestDox('Exists succeeds for OK result when not matching')]
public function testExistsSucceedsForOKResultWhenNotMatching(): void
{
$this->assertFalse(Result::OK(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
}
#[TestDox('Exists succeeds for Error result')]
public function testExistsSucceedsForErrorResult(): void
{
$this->assertFalse(Result::Error(true)->exists(fn($it) => true), 'Exists should have returned false');
} }
#[TestDox('Map succeeds for OK result')] #[TestDox('Map succeeds for OK result')]
@@ -79,8 +153,8 @@ class ResultTest extends TestCase
{ {
$ok = Result::OK('yard'); $ok = Result::OK('yard');
$mapped = $ok->map(fn($it) => strrev($it)); $mapped = $ok->map(fn($it) => strrev($it));
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"'); $this->assertTrue($mapped->isOK, 'The mapped result should be "OK"');
$this->assertEquals('dray', $mapped->getOK(), 'The mapping function was not called correctly'); $this->assertEquals('dray', $mapped->ok, 'The mapping function was not called correctly');
} }
#[TestDox('Map fails for OK result when mapping is null')] #[TestDox('Map fails for OK result when mapping is null')]
@@ -100,7 +174,7 @@ class ResultTest extends TestCase
$tattle->called = true; $tattle->called = true;
return 'hello'; return 'hello';
}); });
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"'); $this->assertTrue($mapped->isError, 'The mapped result should be "Error"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called'); $this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($error, $mapped, 'The same "Error" instance should have been returned'); $this->assertSame($error, $mapped, 'The same "Error" instance should have been returned');
} }
@@ -115,7 +189,7 @@ class ResultTest extends TestCase
$tattle->called = true; $tattle->called = true;
return 'hello'; return 'hello';
}); });
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"'); $this->assertTrue($mapped->isOK, 'The mapped result should be "OK"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called'); $this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned'); $this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned');
} }
@@ -125,8 +199,8 @@ class ResultTest extends TestCase
{ {
$error = Result::Error('taco'); $error = Result::Error('taco');
$mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it))); $mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it)));
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"'); $this->assertTrue($mapped->isError, 'The mapped result should be "Error"');
$this->assertEquals('****', $mapped->getError(), 'The mapping function was not called correctly'); $this->assertEquals('****', $mapped->error, 'The mapping function was not called correctly');
} }
#[TestDox('MapError fails for Error result when mapping is null')] #[TestDox('MapError fails for Error result when mapping is null')]
@@ -152,19 +226,35 @@ class ResultTest extends TestCase
$this->assertNull($target->called, 'The function should not have been called'); $this->assertNull($target->called, 'The function should not have been called');
} }
#[TestDox('ToArray succeeds for OK result')]
public function testToArraySucceedsForOKResult(): void
{
$arr = Result::OK('yay')->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEquals(['yay'], $arr, 'The array was not created correctly');
}
#[TestDox('ToArray succeeds for Error result')]
public function testToArraySucceedsForErrorResult(): void
{
$arr = Result::Error('oh no')->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEmpty($arr, 'The array should have been empty');
}
#[TestDox('ToOption succeeds for OK result')] #[TestDox('ToOption succeeds for OK result')]
public function testToOptionSucceedsForOKResult() public function testToOptionSucceedsForOKResult()
{ {
$value = Result::OK(99)->toOption(); $value = Result::OK(99)->toOption();
$this->assertTrue($value->isSome(), 'An "OK" result should map to a "Some" option'); $this->assertTrue($value->isSome, 'An "OK" result should map to a "Some" option');
$this->assertEquals(99, $value->get(), 'The value is not correct'); $this->assertEquals(99, $value->value, 'The value is not correct');
} }
#[TestDox('ToOption succeeds for Error result')] #[TestDox('ToOption succeeds for Error result')]
public function testToOptionSucceedsForErrorResult() public function testToOptionSucceedsForErrorResult()
{ {
$value = Result::Error('file not found')->toOption(); $value = Result::Error('file not found')->toOption();
$this->assertTrue($value->isNone(), 'An "Error" result should map to a "None" option'); $this->assertTrue($value->isNone, 'An "Error" result should map to a "None" option');
} }
#[TestDox('Tap succeeds for OK result')] #[TestDox('Tap succeeds for OK result')]
@@ -173,7 +263,7 @@ class ResultTest extends TestCase
$value = ''; $value = '';
$original = Result::OK('working'); $original = Result::OK('working');
$tapped = $original->tap(function (Result $it) use (&$value) { $tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError(); $value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
}); });
$this->assertEquals('OK: working', $value, 'The tapped function was not called'); $this->assertEquals('OK: working', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned'); $this->assertSame($original, $tapped, 'The same result should have been returned');
@@ -185,7 +275,7 @@ class ResultTest extends TestCase
$value = ''; $value = '';
$original = Result::Error('failed'); $original = Result::Error('failed');
$tapped = $original->tap(function (Result $it) use (&$value) { $tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError(); $value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
}); });
$this->assertEquals('Error: failed', $value, 'The tapped function was not called'); $this->assertEquals('Error: failed', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned'); $this->assertSame($original, $tapped, 'The same result should have been returned');
@@ -195,8 +285,8 @@ class ResultTest extends TestCase
public function testOKSucceedsForNonNullResult(): void public function testOKSucceedsForNonNullResult(): void
{ {
$result = Result::OK('something'); $result = Result::OK('something');
$this->assertTrue($result->isOK(), 'The result should have been "OK"'); $this->assertTrue($result->isOK, 'The result should have been "OK"');
$this->assertEquals('something', $result->getOK(), 'The "OK" value was incorrect'); $this->assertEquals('something', $result->ok, 'The "OK" value was incorrect');
} }
#[TestDox('OK fails for null result')] #[TestDox('OK fails for null result')]
@@ -210,8 +300,8 @@ class ResultTest extends TestCase
public function testErrorSucceedsForNonNullResult(): void public function testErrorSucceedsForNonNullResult(): void
{ {
$result = Result::Error('sad trombone'); $result = Result::Error('sad trombone');
$this->assertTrue($result->isError(), 'The result should have been "Error"'); $this->assertTrue($result->isError, 'The result should have been "Error"');
$this->assertEquals('sad trombone', $result->getError(), 'The "Error" value was incorrect'); $this->assertEquals('sad trombone', $result->error, 'The "Error" value was incorrect');
} }
#[TestDox('Error fails for null result')] #[TestDox('Error fails for null result')]