Repository: karen-irc/option-t (js)

Issue:
Implement orElseAsync for PlainOption

Pull request:
ImplementorElseAsync() for PlainOption
Fix #929

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 3fb91f67..5a233eb5 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -80,6 +80,7 @@
 - [option-t/PlainOption/Option](../src/PlainOption/Option.ts)
 - [option-t/PlainOption/or](../src/PlainOption/or.ts)
 - [option-t/PlainOption/orElse](../src/PlainOption/orElse.ts)
+- [option-t/PlainOption/orElseAsync](../src/PlainOption/orElseAsync.ts)
 - [option-t/PlainOption/transpose](../src/PlainOption/transpose.ts)
 - [option-t/PlainOption/tap](../src/PlainOption/tap.ts)
 - [option-t/PlainOption/unwrap](../src/PlainOption/unwrap.ts)
diff --git a/src/PlainOption/index.ts b/src/PlainOption/index.ts
index 191db00f..246617a6 100644
--- a/src/PlainOption/index.ts
+++ b/src/PlainOption/index.ts
@@ -39,6 +39,7 @@ export { mapOrForOption as mapOr } from './mapOr';
 export { mapOrElseForOption as mapOrElse } from './mapOrElse';
 export { orForOption as or } from './or';
 export { orElseForOption as orElse } from './orElse';
+export { orElseAsyncForOption as orElseAsync } from './orElseAsync';
 export { transposeForOption as transpose } from './transpose';
 export { tapOption as tap } from './tap';
 export { unwrapOption as unwrap } from './unwrap';
diff --git a/src/PlainOption/orElseAsync.ts b/src/PlainOption/orElseAsync.ts
new file mode 100644
index 00000000..84f00666
--- /dev/null
+++ b/src/PlainOption/orElseAsync.ts
@@ -0,0 +1,26 @@
+import { assertIsPromise } from '../internal/assert';
+import { ERR_MSG_RECOVERER_MUST_RETURN_PROMISE } from '../internal/ErrorMessage';
+import type { AsyncRecoveryFn } from '../internal/Function';
+import { Option, isSome } from './Option';
+
+export type OptionAsyncTryRecoveryFn<T> = AsyncRecoveryFn<Option<T>>;
+
+/**
+ *  Return _input_ as `T` if the passed _input_ is `Some(T)`.
+ *  Otherwise, return the result of _recoverer_.
+ */
+export function orElseAsyncForOption<T>(
+    input: Option<T>,
+    recoverer: OptionAsyncTryRecoveryFn<T>
+): Promise<Option<T>> {
+    if (isSome(input)) {
+        return Promise.resolve(input);
+    }
+
+    const defaultValue = recoverer();
+    // If this is async function, this always return Promise, but not.
+    // We should check to clarify the error case if user call this function from plain js
+    // and they mistake to use this.
+    assertIsPromise(defaultValue, ERR_MSG_RECOVERER_MUST_RETURN_PROMISE);
+    return defaultValue;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index c9394f5f..627227ad 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -147,6 +147,8 @@
     "cjs/PlainOption/or.js": null,
     "cjs/PlainOption/orElse.d.ts": null,
     "cjs/PlainOption/orElse.js": null,
+    "cjs/PlainOption/orElseAsync.d.ts": null,
+    "cjs/PlainOption/orElseAsync.js": null,
     "cjs/PlainOption/transpose.d.ts": null,
     "cjs/PlainOption/transpose.js": null,
     "cjs/PlainOption/tap.d.ts": null,
@@ -415,6 +417,8 @@
     "esm/PlainOption/or.mjs": null,
     "esm/PlainOption/orElse.d.ts": null,
     "esm/PlainOption/orElse.mjs": null,
+    "esm/PlainOption/orElseAsync.d.ts": null,
+    "esm/PlainOption/orElseAsync.mjs": null,
     "esm/PlainOption/transpose.d.ts": null,
     "esm/PlainOption/transpose.mjs": null,
     "esm/PlainOption/tap.d.ts": null,
@@ -751,6 +755,9 @@
     "lib/PlainOption/orElse.d.ts": null,
     "lib/PlainOption/orElse.js": null,
     "lib/PlainOption/orElse.mjs": null,
+    "lib/PlainOption/orElseAsync.d.ts": null,
+    "lib/PlainOption/orElseAsync.js": null,
+    "lib/PlainOption/orElseAsync.mjs": null,
     "lib/PlainOption/transpose.d.ts": null,
     "lib/PlainOption/transpose.js": null,
     "lib/PlainOption/transpose.mjs": null,
diff --git a/tools/public_api/table.mjs b/tools/public_api/table.mjs
index d34e494b..eabf15fc 100644
--- a/tools/public_api/table.mjs
+++ b/tools/public_api/table.mjs
@@ -274,6 +274,7 @@ export const apiTable = Object.freeze({
             'mapOrElse',
             'or',
             'orElse',
+            'orElseAsync',
             'transpose',
             'tap',
             'unwrap',
@@ -375,6 +376,11 @@ export const apiTable = Object.freeze({
             'orElseForOption'
         ]
     },
+    'PlainOption/orElseAsync': {
+        'exports': [
+            'orElseAsyncForOption'
+        ]
+    },
     'PlainOption/transpose': {
         'exports': [
             'transposeForOption'
@@ -805,6 +811,7 @@ export const legacyApiTable = Object.freeze({
     'cjs/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/or': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/orElse': COMPAT_DESCRIPTOR,
+    'cjs/PlainOption/orElseAsync': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/transpose': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/tap': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/unwrap': COMPAT_DESCRIPTOR,
@@ -934,6 +941,7 @@ export const legacyApiTable = Object.freeze({
     'esm/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'esm/PlainOption/or': COMPAT_DESCRIPTOR,
     'esm/PlainOption/orElse': COMPAT_DESCRIPTOR,
+    'esm/PlainOption/orElseAsync': COMPAT_DESCRIPTOR,
     'esm/PlainOption/transpose': COMPAT_DESCRIPTOR,
     'esm/PlainOption/tap': COMPAT_DESCRIPTOR,
     'esm/PlainOption/unwrap': COMPAT_DESCRIPTOR,
@@ -1063,6 +1071,7 @@ export const legacyApiTable = Object.freeze({
     'lib/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'lib/PlainOption/or': COMPAT_DESCRIPTOR,
     'lib/PlainOption/orElse': COMPAT_DESCRIPTOR,
+    'lib/PlainOption/orElseAsync': COMPAT_DESCRIPTOR,
     'lib/PlainOption/transpose': COMPAT_DESCRIPTOR,
     'lib/PlainOption/tap': COMPAT_DESCRIPTOR,
     'lib/PlainOption/unwrap': COMPAT_DESCRIPTOR,


Test patch:
diff --git a/__tests__/PlainOption/and_then_async.test.mjs b/__tests__/PlainOption/and_then_async.test.mjs
index 6786a27b..6117b978 100644
--- a/__tests__/PlainOption/and_then_async.test.mjs
+++ b/__tests__/PlainOption/and_then_async.test.mjs
@@ -5,7 +5,6 @@ import { andThenAsyncForOption } from '../../__dist/esm/PlainOption/andThenAsync
 
 const VALUE_T = Math.random();
 const VALUE_U = Math.random();
-const ERROR_E = new Error('e');
 
 test('input is Some(T), callback return Some(U)', async (t) => {
     t.plan(5);
diff --git a/__tests__/PlainOption/or_else_async.test.mjs b/__tests__/PlainOption/or_else_async.test.mjs
new file mode 100644
index 00000000..32e10d3f
--- /dev/null
+++ b/__tests__/PlainOption/or_else_async.test.mjs
@@ -0,0 +1,74 @@
+import test from 'ava';
+
+import { createSome, createNone, isSome, isNone } from '../../__dist/esm/PlainOption/Option.mjs';
+import { orElseAsyncForOption } from '../../__dist/esm/PlainOption/orElseAsync.mjs';
+import { unwrapOption } from '../../__dist/esm/PlainOption/unwrap.mjs';
+
+const VALUE_T = Math.random();
+const VALUE_U = Math.random();
+
+test('input is Some(T)', async (t) => {
+    t.plan(2);
+
+    const input = createSome(VALUE_T);
+    const result = orElseAsyncForOption(input, async () => {
+        t.pass(false);
+        return createSome(VALUE_U);
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, input);
+});
+
+test('input is None, callback return Some(T)', async (t) => {
+    t.plan(4);
+
+    const input = createNone();
+    const result = orElseAsyncForOption(input, async () => {
+        t.pass(true);
+        return createSome(VALUE_T);
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.true(isSome(actual));
+    t.is(unwrapOption(actual), VALUE_T);
+});
+
+test('input is None, callback return None', async (t) => {
+    t.plan(4);
+
+    const input = createNone();
+
+    const result = orElseAsyncForOption(input, async () => {
+        t.pass(true);
+        return createNone();
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.true(isNone(actual));
+    t.not(actual, input);
+});
+
+test('callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const input = createNone();
+            await orElseAsyncForOption(input, () => {
+                t.pass();
+                return createNone();
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: '`recoverer` must return Promise',
+        }
+    );
+});