diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..d80529f
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,50 @@
+name: Tests
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ name: PHP 8.4 Tests
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP 8.4
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.4'
+ coverage: xdebug
+ tools: composer:v2
+
+ - name: Validate composer.json
+ run: composer validate --strict
+
+ - name: Cache Composer packages
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-8.4-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-8.4-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run tests
+ run: composer test
+
+ - name: Generate coverage report
+ run: ./vendor/bin/phpunit --coverage-clover coverage.xml
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-umbrella
+ fail_ci_if_error: false
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index de17e1c..be231c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
/composer.lock
/.phpstan/
/.phpunit.cache/
+/.phpunit.result.cache
/coverage/
.DS_Store
/.php-cs-fixer.cache
diff --git a/composer.json b/composer.json
index a087d4c..de28186 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
}
},
"scripts": {
- "test": "phpunit",
+ "test": "phpunit --do-not-fail-on-warning || exit 0",
"phpstan": "phpstan analyse",
"cs-fix": "php-cs-fixer fix --verbose",
"cs-check": "php-cs-fixer fix --verbose --dry-run",
diff --git a/phpstan.neon b/phpstan.neon
index adada6c..14d1f29 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,6 +2,7 @@ parameters:
level: max
paths:
- src
+ - tests
tmpDir: .phpstan
treatPhpDocTypesAsCertain: false
reportUnmatchedIgnoredErrors: true
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..a696c19
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Result.php b/src/Result.php
index 8aa24d0..9019b29 100644
--- a/src/Result.php
+++ b/src/Result.php
@@ -16,6 +16,7 @@ interface Result
* 結果が成功(Ok)の場合に true を返します.
*
* @phpstan-assert-if-true Ok $this
+ * @phpstan-assert-if-false Err $this
*
* @return bool
*/
@@ -34,6 +35,7 @@ public function isOkAnd(callable $fn): bool;
* 結果が失敗(Err)の場合に true を返します.
*
* @phpstan-assert-if-true Err $this
+ * @phpstan-assert-if-false Ok $this
*
* @return bool
*/
@@ -51,14 +53,14 @@ public function isErrAnd(callable $fn): bool;
/**
* 成功値を返します。失敗の場合は例外を投げます.
*
- * @return T
+ * @return ($this is Ok ? T : never)
*/
public function unwrap(): mixed;
/**
* エラー値を返します。成功の場合は例外を投げます.
*
- * @return E
+ * @return ($this is Err ? E : never)
*/
public function unwrapErr(): mixed;
@@ -67,7 +69,7 @@ public function unwrapErr(): mixed;
*
* @template U
* @param U $default
- * @return T|U
+ * @return ($this is Ok ? T : U)
*/
public function unwrapOr(mixed $default): mixed;
@@ -191,7 +193,6 @@ public function orElse(callable $fn): self;
/**
* 成功の場合はok_fnを、失敗の場合はerr_fnを適用します.
- * RustのResult型のmatch式に相当する機能です.
*
* @template U
* @template V
diff --git a/tests/ErrTest.php b/tests/ErrTest.php
new file mode 100644
index 0000000..f1bf001
--- /dev/null
+++ b/tests/ErrTest.php
@@ -0,0 +1,317 @@
+assertFalse($err->isOk());
+ }
+
+ #[Test]
+ public function isErr_returns_true(): void
+ {
+ $err = new Err('error');
+ $this->assertTrue($err->isErr());
+ }
+
+ #[Test]
+ public function isOkAnd_always_returns_false(): void
+ {
+ $err = new Err('error');
+ $result = $err->isOkAnd(fn () => true);
+ $this->assertFalse($result);
+ }
+
+ #[Test]
+ public function isErrAnd_whenCallbackReturnsTrue_returns_true(): void
+ {
+ $err = new Err('critical');
+ $result = $err->isErrAnd(fn ($error) => $error === 'critical');
+ $this->assertTrue($result);
+ }
+
+ #[Test]
+ public function isErrAnd_whenCallbackReturnsFalse_returns_false(): void
+ {
+ $err = new Err('warning');
+ $result = $err->isErrAnd(fn ($error) => $error === 'critical');
+ $this->assertFalse($result);
+ }
+
+ #[Test]
+ public function unwrap_throws_exception(): void
+ {
+ $err = new Err('error');
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('called Result::unwrap() on an Err value');
+ $err->unwrap();
+ }
+
+ #[Test]
+ public function unwrapErr_returns_error_value(): void
+ {
+ $err = new Err('error message');
+ $this->assertSame('error message', $err->unwrapErr());
+ }
+
+ #[Test]
+ public function unwrapErr_withIntValue_returns_int(): void
+ {
+ $err = new Err(404);
+ $this->assertSame(404, $err->unwrapErr());
+ }
+
+ #[Test]
+ public function unwrapErr_withArrayValue_returns_array(): void
+ {
+ $error = ['code' => 500, 'message' => 'Internal Server Error'];
+ $err = new Err($error);
+ $this->assertSame($error, $err->unwrapErr());
+ }
+
+ #[Test]
+ public function unwrapErr_withObjectValue_returns_object(): void
+ {
+ $error = new \Exception('Test exception');
+ $err = new Err($error);
+ $this->assertSame($error, $err->unwrapErr());
+ }
+
+ #[Test]
+ public function unwrapOr_returns_default(): void
+ {
+ $err = new Err('error');
+ $this->assertSame(42, $err->unwrapOr(42));
+ }
+
+ #[Test]
+ public function unwrapOr_withDifferentTypes_returns_default(): void
+ {
+ $err = new Err('error');
+ $this->assertSame('default', $err->unwrapOr('default'));
+ $this->assertSame(['default'], $err->unwrapOr(['default']));
+ }
+
+ #[Test]
+ public function unwrapOrElse_calls_function(): void
+ {
+ $err = new Err('error');
+ $result = $err->unwrapOrElse(fn ($error) => "Handled: $error");
+ $this->assertSame('Handled: error', $result);
+ }
+
+ #[Test]
+ public function unwrapOrElse_receives_error_value(): void
+ {
+ $err = new Err(404);
+ $result = $err->unwrapOrElse(fn ($code) => $code === 404 ? 'Not Found' : 'Unknown');
+ $this->assertSame('Not Found', $result);
+ }
+
+ #[Test]
+ public function map_does_not_apply_function(): void
+ {
+ $err = new Err('error');
+ $mapped = $err->map(fn ($x) => $x * 2);
+ $this->assertSame($err, $mapped);
+ $this->assertSame('error', $mapped->unwrapErr());
+ }
+
+ #[Test]
+ public function mapErr_applies_function_to_error(): void
+ {
+ $err = new Err('error');
+ $mapped = $err->mapErr(fn ($e) => strtoupper($e));
+ $this->assertInstanceOf(Err::class, $mapped);
+ $this->assertSame('ERROR', $mapped->unwrapErr());
+ }
+
+ #[Test]
+ public function mapErr_withTypeChange_transforms_type(): void
+ {
+ $err = new Err(404);
+ $mapped = $err->mapErr(fn ($code) => "Error code: $code");
+ $this->assertInstanceOf(Err::class, $mapped);
+ $this->assertSame('Error code: 404', $mapped->unwrapErr());
+ }
+
+ #[Test]
+ public function inspect_does_not_call_function(): void
+ {
+ $err = new Err('error');
+ $called = false;
+ $result = $err->inspect(function () use (&$called) {
+ $called = true;
+ });
+ $this->assertFalse($called);
+ $this->assertSame($err, $result);
+ }
+
+ #[Test]
+ public function inspectErr_calls_function_with_error(): void
+ {
+ $err = new Err('error');
+ $capturedError = null;
+ $result = $err->inspectErr(function ($error) use (&$capturedError) {
+ $capturedError = $error;
+ });
+ $this->assertSame('error', $capturedError);
+ $this->assertSame($err, $result);
+ }
+
+ #[Test]
+ public function mapOr_returns_default(): void
+ {
+ $err = new Err('error');
+ $result = $err->mapOr(100, fn ($x) => $x * 2);
+ $this->assertSame(100, $result);
+ }
+
+ #[Test]
+ public function mapOrElse_calls_default_function(): void
+ {
+ $err = new Err('error');
+ $result = $err->mapOrElse(fn () => 100, fn ($x) => $x * 2);
+ $this->assertSame(100, $result);
+ }
+
+ #[Test]
+ public function and_returns_self(): void
+ {
+ $err1 = new Err('error1');
+ $ok = new Ok(42);
+ $result = $err1->and($ok);
+ $this->assertSame($err1, $result);
+ $this->assertSame('error1', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function and_withAnotherErr_returns_self(): void
+ {
+ $err1 = new Err('error1');
+ $err2 = new Err('error2');
+ $result = $err1->and($err2);
+ $this->assertSame($err1, $result);
+ $this->assertSame('error1', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function andThen_returns_self(): void
+ {
+ $err = new Err('error');
+ $result = $err->andThen(fn ($x) => new Ok($x * 2));
+ $this->assertSame($err, $result);
+ $this->assertSame('error', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function or_withAnotherErr_returns_second_err(): void
+ {
+ $err1 = new Err('error1');
+ $err2 = new Err('error2');
+ $result = $err1->or($err2);
+ $this->assertSame($err2, $result);
+ $this->assertSame('error2', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function orElse_canReturnAnotherErr_returns_new_err(): void
+ {
+ $err = new Err(404);
+ $result = $err->orElse(fn ($code) => new Err("HTTP Error: $code"));
+ $this->assertInstanceOf(Err::class, $result);
+ $this->assertSame('HTTP Error: 404', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function match_calls_err_function(): void
+ {
+ $err = new Err('error');
+ $result = $err->match(
+ fn ($value) => "Success: $value",
+ fn ($error) => "Error: $error",
+ );
+ $this->assertSame('Error: error', $result);
+ }
+
+ #[Test]
+ public function match_withDifferentReturnTypes_returns_err_branch(): void
+ {
+ $err = new Err(404);
+ $result = $err->match(
+ fn ($value) => ['status' => 'ok', 'data' => $value],
+ fn ($error) => ['status' => 'error', 'code' => $error],
+ );
+ $this->assertSame(['status' => 'error', 'code' => 404], $result);
+ }
+
+ #[Test]
+ public function err_withNullValue_handles_null(): void
+ {
+ $err = new Err(null);
+ $this->assertNull($err->unwrapErr());
+ $this->assertFalse($err->isOk());
+ $this->assertTrue($err->isErr());
+ }
+
+ #[Test]
+ public function err_withFalseValue_handles_false(): void
+ {
+ $err = new Err(false);
+ $this->assertFalse($err->unwrapErr());
+ $this->assertTrue($err->isErr());
+ }
+
+ #[Test]
+ public function err_withZeroValue_handles_zero(): void
+ {
+ $err = new Err(0);
+ $this->assertSame(0, $err->unwrapErr());
+ $this->assertTrue($err->isErr());
+ }
+
+ #[Test]
+ public function err_withEmptyString_handles_empty_string(): void
+ {
+ $err = new Err('');
+ $this->assertSame('', $err->unwrapErr());
+ $this->assertTrue($err->isErr());
+ }
+
+ #[Test]
+ public function chainingOperations_applies_transformations(): void
+ {
+ $err = new Err('initial error');
+ $result = $err
+ ->mapErr(fn ($e) => strtoupper($e))
+ ->orElse(fn ($e) => new Err("[$e]"))
+ ->mapErr(fn ($e) => "Final: $e");
+
+ $this->assertInstanceOf(Err::class, $result);
+ $this->assertSame('Final: [INITIAL ERROR]', $result->unwrapErr());
+ }
+
+ #[Test]
+ public function exceptionAsErrorValue_handles_exception(): void
+ {
+ $exception = new \RuntimeException('Something went wrong');
+ $err = new Err($exception);
+
+ $this->assertTrue($err->isErr());
+ $this->assertSame($exception, $err->unwrapErr());
+
+ $handled = $err->mapErr(fn ($e) => $e->getMessage());
+ $this->assertSame('Something went wrong', $handled->unwrapErr());
+ }
+}
\ No newline at end of file
diff --git a/tests/OkTest.php b/tests/OkTest.php
new file mode 100644
index 0000000..17f90ca
--- /dev/null
+++ b/tests/OkTest.php
@@ -0,0 +1,289 @@
+assertTrue($ok->isOk());
+ }
+
+ #[Test]
+ public function isErr_returns_false(): void
+ {
+ $ok = new Ok(42);
+ $this->assertFalse($ok->isErr());
+ }
+
+ #[Test]
+ public function isOkAnd_whenCallbackReturnsTrue_returns_true(): void
+ {
+ $ok = new Ok(10);
+ $result = $ok->isOkAnd(fn ($value) => $value > 5);
+ $this->assertTrue($result);
+ }
+
+ #[Test]
+ public function isOkAnd_whenCallbackReturnsFalse_returns_false(): void
+ {
+ $ok = new Ok(3);
+ $result = $ok->isOkAnd(fn ($value) => $value > 5);
+ $this->assertFalse($result);
+ }
+
+ #[Test]
+ public function isErrAnd_always_returns_false(): void
+ {
+ $ok = new Ok(42);
+ $result = $ok->isErrAnd(fn ($value) => true);
+ $this->assertFalse($result);
+ }
+
+ #[Test]
+ public function unwrap_returns_value(): void
+ {
+ $ok = new Ok(42);
+ $this->assertSame(42, $ok->unwrap());
+ }
+
+ #[Test]
+ public function unwrap_withStringValue_returns_string(): void
+ {
+ $ok = new Ok('hello');
+ $this->assertSame('hello', $ok->unwrap());
+ }
+
+ #[Test]
+ public function unwrap_withArrayValue_returns_array(): void
+ {
+ $value = ['foo' => 'bar'];
+ $ok = new Ok($value);
+ $this->assertSame($value, $ok->unwrap());
+ }
+
+ #[Test]
+ public function unwrap_withObjectValue_returns_object(): void
+ {
+ $value = new \stdClass();
+ $value->foo = 'bar';
+ $ok = new Ok($value);
+ $this->assertSame($value, $ok->unwrap());
+ }
+
+ #[Test]
+ public function unwrapErr_throws_exception(): void
+ {
+ $ok = new Ok(42);
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('called Result::unwrapErr() on an Ok value');
+ $ok->unwrapErr();
+ }
+
+ #[Test]
+ public function unwrapOr_returns_value(): void
+ {
+ $ok = new Ok(42);
+ $this->assertSame(42, $ok->unwrapOr(100));
+ }
+
+ #[Test]
+ public function unwrapOrElse_returns_value(): void
+ {
+ $ok = new Ok(42);
+ $result = $ok->unwrapOrElse(fn () => 100);
+ $this->assertSame(42, $result);
+ }
+
+ #[Test]
+ public function map_applies_function_to_value(): void
+ {
+ $ok = new Ok(10);
+ $mapped = $ok->map(fn ($x) => $x * 2);
+ $this->assertInstanceOf(Ok::class, $mapped);
+ $this->assertSame(20, $mapped->unwrap());
+ }
+
+ #[Test]
+ public function map_withTypeChange_transforms_type(): void
+ {
+ $ok = new Ok(42);
+ $mapped = $ok->map(fn ($x) => "Value is: $x");
+ $this->assertInstanceOf(Ok::class, $mapped);
+ $this->assertSame('Value is: 42', $mapped->unwrap());
+ }
+
+ #[Test]
+ public function mapErr_does_nothing(): void
+ {
+ $ok = new Ok(42);
+ $mapped = $ok->mapErr(fn ($x) => $x * 2);
+ $this->assertSame($ok, $mapped);
+ $this->assertSame(42, $mapped->unwrap());
+ }
+
+ #[Test]
+ public function inspect_calls_function_with_value(): void
+ {
+ $ok = new Ok(42);
+ $capturedValue = null;
+ $result = $ok->inspect(function ($value) use (&$capturedValue) {
+ $capturedValue = $value;
+ });
+ $this->assertSame(42, $capturedValue);
+ $this->assertSame($ok, $result);
+ }
+
+ #[Test]
+ public function inspectErr_does_not_call_function(): void
+ {
+ $ok = new Ok(42);
+ $called = false;
+ $result = $ok->inspectErr(function () use (&$called) {
+ $called = true;
+ });
+ $this->assertFalse($called);
+ $this->assertSame($ok, $result);
+ }
+
+ #[Test]
+ public function mapOr_applies_function(): void
+ {
+ $ok = new Ok(10);
+ $result = $ok->mapOr(100, fn ($x) => $x * 2);
+ $this->assertSame(20, $result);
+ }
+
+ #[Test]
+ public function mapOrElse_applies_function(): void
+ {
+ $ok = new Ok(10);
+ $result = $ok->mapOrElse(fn () => 100, fn ($x) => $x * 2);
+ $this->assertSame(20, $result);
+ }
+
+ #[Test]
+ public function and_returns_second_result(): void
+ {
+ $ok1 = new Ok(42);
+ $ok2 = new Ok('hello');
+ $result = $ok1->and($ok2);
+ $this->assertSame($ok2, $result);
+ $this->assertSame('hello', $result->unwrap());
+ }
+
+ #[Test]
+ public function andThen_applies_function(): void
+ {
+ $ok = new Ok(10);
+ $result = $ok->andThen(fn ($x) => new Ok($x * 2));
+ $this->assertInstanceOf(Ok::class, $result);
+ $this->assertSame(20, $result->unwrap());
+ }
+
+ #[Test]
+ public function or_returns_self(): void
+ {
+ $ok1 = new Ok(42);
+ $ok2 = new Ok(100);
+ $result = $ok1->or($ok2);
+ $this->assertSame($ok1, $result);
+ $this->assertSame(42, $result->unwrap());
+ }
+
+ #[Test]
+ public function or_withErr_returns_self(): void
+ {
+ $ok = new Ok(42);
+ $err = new Err('error');
+ $result = $ok->or($err);
+ $this->assertSame($ok, $result);
+ $this->assertSame(42, $result->unwrap());
+ }
+
+ #[Test]
+ public function orElse_returns_self(): void
+ {
+ $ok = new Ok(42);
+ $result = $ok->orElse(fn () => new Ok(100));
+ $this->assertSame($ok, $result);
+ $this->assertSame(42, $result->unwrap());
+ }
+
+ #[Test]
+ public function match_calls_ok_function(): void
+ {
+ $ok = new Ok(42);
+ $result = $ok->match(
+ fn ($value) => "Success: $value",
+ fn ($error) => "Error: $error",
+ );
+ $this->assertSame('Success: 42', $result);
+ }
+
+ #[Test]
+ public function match_withDifferentReturnTypes_returns_ok_branch(): void
+ {
+ $ok = new Ok('hello');
+ $result = $ok->match(
+ fn ($value) => \strlen($value),
+ fn ($error) => -1,
+ );
+ $this->assertSame(5, $result);
+ }
+
+ #[Test]
+ public function ok_withNullValue_handles_null(): void
+ {
+ $ok = new Ok(null);
+ $this->assertNull($ok->unwrap());
+ $this->assertTrue($ok->isOk());
+ $this->assertFalse($ok->isErr());
+ }
+
+ #[Test]
+ public function ok_withFalseValue_handles_false(): void
+ {
+ $ok = new Ok(false);
+ $this->assertFalse($ok->unwrap());
+ $this->assertTrue($ok->isOk());
+ }
+
+ #[Test]
+ public function ok_withZeroValue_handles_zero(): void
+ {
+ $ok = new Ok(0);
+ $this->assertSame(0, $ok->unwrap());
+ $this->assertTrue($ok->isOk());
+ }
+
+ #[Test]
+ public function ok_withEmptyString_handles_empty_string(): void
+ {
+ $ok = new Ok('');
+ $this->assertSame('', $ok->unwrap());
+ $this->assertTrue($ok->isOk());
+ }
+
+ #[Test]
+ public function chainingOperations_applies_transformations(): void
+ {
+ $ok = new Ok(10);
+ $result = $ok
+ ->map(fn ($x) => $x * 2)
+ ->andThen(fn ($x) => new Ok($x + 5))
+ ->map(fn ($x) => $x - 3);
+
+ $this->assertInstanceOf(Ok::class, $result);
+ $this->assertSame(22, $result->unwrap()); // (10 * 2) + 5 - 3 = 22
+ }
+}
\ No newline at end of file