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

Issue:
Implement unwrapOrElseAsync for Maybe

Pull request description:
Impl unwrap or else async maybe
Fix #936

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 0e612bf1..14fe93c2 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -28,6 +28,7 @@
 - [option-t/Maybe/unwrap](../src/Maybe/unwrap.ts)
 - [option-t/Maybe/unwrapOr](../src/Maybe/unwrapOr.ts)
 - [option-t/Maybe/unwrapOrElse](../src/Maybe/unwrapOrElse.ts)
+- [option-t/Maybe/unwrapOrElseAsync](../src/Maybe/unwrapOrElseAsync.ts)
 - [option-t/Maybe/xor](../src/Maybe/xor.ts)
 
 
diff --git a/src/Maybe/index.ts b/src/Maybe/index.ts
index 90900247..38b7b8c4 100644
--- a/src/Maybe/index.ts
+++ b/src/Maybe/index.ts
@@ -16,3 +16,4 @@ export { tapMaybe as tap } from './tap';
 export { unwrapMaybe as unwrap } from './unwrap';
 export { unwrapOrFromMaybe as unwrapOr } from './unwrapOr';
 export { unwrapOrElseFromMaybe as unwrapOrElse } from './unwrapOrElse';
+export { unwrapOrElseAsyncFromMaybe as unwrapOrElseAsync } from './unwrapOrElseAsync';
diff --git a/src/Maybe/unwrapOrElseAsync.ts b/src/Maybe/unwrapOrElseAsync.ts
new file mode 100644
index 00000000..f6df016f
--- /dev/null
+++ b/src/Maybe/unwrapOrElseAsync.ts
@@ -0,0 +1,41 @@
+import { assertIsPromise } from '../internal/assert';
+import { ERR_MSG_RECOVERER_MUST_RETURN_PROMISE } from '../internal/ErrorMessage';
+import type { AsyncRecoveryFn } from '../internal/Function';
+
+import { Maybe, isNotNullAndUndefined } from './Maybe';
+import { expectNotNullAndUndefined } from './expect';
+import { ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE } from './ErrorMessage';
+
+function check<T>(value: Maybe<T>): T {
+    const passed = expectNotNullAndUndefined(
+        value,
+        ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE
+    );
+    return passed;
+}
+
+/**
+ *  Return _input_ as `T` if the passed _input_ is not `null` and `undefined`.
+ *  Otherwise, return the result of _recoverer_.
+ *
+ *  * The result of _recoverer_ must not be `Maybe<*>`.
+ *  * If the result of _recoverer_ is `null` or `undefined`, throw `TypeError`.
+ */
+export function unwrapOrElseAsyncFromMaybe<T>(
+    input: Maybe<T>,
+    recoverer: AsyncRecoveryFn<T>
+): Promise<T> {
+    if (isNotNullAndUndefined(input)) {
+        return Promise.resolve(input);
+    }
+
+    const fallback: Promise<T> = 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(fallback, ERR_MSG_RECOVERER_MUST_RETURN_PROMISE);
+
+    const passed = fallback.then(check);
+    return passed;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 318bfeee..7e4220ae 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -87,6 +87,8 @@
     "cjs/Maybe/unwrapOr.js": null,
     "cjs/Maybe/unwrapOrElse.d.ts": null,
     "cjs/Maybe/unwrapOrElse.js": null,
+    "cjs/Maybe/unwrapOrElseAsync.d.ts": null,
+    "cjs/Maybe/unwrapOrElseAsync.js": null,
     "cjs/Maybe/xor.d.ts": null,
     "cjs/Maybe/xor.js": null,
     "cjs/Nullable/ErrorMessage.d.ts": null,
@@ -377,6 +379,8 @@
     "esm/Maybe/unwrapOr.mjs": null,
     "esm/Maybe/unwrapOrElse.d.ts": null,
     "esm/Maybe/unwrapOrElse.mjs": null,
+    "esm/Maybe/unwrapOrElseAsync.d.ts": null,
+    "esm/Maybe/unwrapOrElseAsync.mjs": null,
     "esm/Maybe/xor.d.ts": null,
     "esm/Maybe/xor.mjs": null,
     "esm/Nullable/ErrorMessage.d.ts": null,
@@ -705,6 +709,9 @@
     "lib/Maybe/unwrapOrElse.d.ts": null,
     "lib/Maybe/unwrapOrElse.js": null,
     "lib/Maybe/unwrapOrElse.mjs": null,
+    "lib/Maybe/unwrapOrElseAsync.d.ts": null,
+    "lib/Maybe/unwrapOrElseAsync.js": null,
+    "lib/Maybe/unwrapOrElseAsync.mjs": null,
     "lib/Maybe/xor.d.ts": null,
     "lib/Maybe/xor.js": null,
     "lib/Maybe/xor.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index a77eab39..96c96dcd 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -22,6 +22,7 @@ export default {
             'unwrap',
             'unwrapOr',
             'unwrapOrElse',
+            'unwrapOrElseAsync',
         ]
     },
     'Maybe/and': {
@@ -105,6 +106,11 @@ export default {
             'unwrapOrElseFromMaybe'
         ]
     },
+    'Maybe/unwrapOrElseAsync': {
+        'exports': [
+            'unwrapOrElseAsyncFromMaybe'
+        ]
+    },
     'Maybe/xor': {
         'exports': [
             'xorForMaybe'


Test patch:
diff --git a/__tests__/Maybe/unwrap_or_else.test.mjs b/__tests__/Maybe/unwrap_or_else.test.mjs
index 975fb59f..499587f5 100644
--- a/__tests__/Maybe/unwrap_or_else.test.mjs
+++ b/__tests__/Maybe/unwrap_or_else.test.mjs
@@ -31,23 +31,18 @@ for (const NULL_VALUE of [undefined, null]) {
 
         t.is(result, DEFAULT_VAL);
     });
-}
 
-{
-    const testcases = [
-        [undefined, undefined],
-        [undefined, null],
-        [null, undefined],
-        [null, null],
-    ];
-    for (const [src, def] of testcases) {
-        test(`should not accept Maybe<*> as default, v = ${String(src)}, def = ${String(
-            def
+    for (const FALLBACK_VALUE of [undefined, null]) {
+        test(`should not accept Maybe<*> as default, v = ${String(NULL_VALUE)}, def = ${String(
+            FALLBACK_VALUE
         )}`, (t) => {
-            t.plan(1);
+            t.plan(2);
             t.throws(
                 () => {
-                    unwrapOrElseFromMaybe(src, () => def);
+                    unwrapOrElseFromMaybe(NULL_VALUE, () => {
+                        t.pass('should be called');
+                        return FALLBACK_VALUE;
+                    });
                 },
                 {
                     instanceOf: TypeError,
diff --git a/__tests__/Maybe/unwrap_or_else_async.test.mjs b/__tests__/Maybe/unwrap_or_else_async.test.mjs
new file mode 100644
index 00000000..4e775f60
--- /dev/null
+++ b/__tests__/Maybe/unwrap_or_else_async.test.mjs
@@ -0,0 +1,73 @@
+import test from 'ava';
+
+import { unwrapOrElseAsyncFromMaybe } from '../../__dist/esm/Maybe/unwrapOrElseAsync.mjs';
+import { nonNullableValueCaseListForAsync } from '../utils.mjs';
+
+for (const [INPUT, , EXPECTED] of nonNullableValueCaseListForAsync) {
+    test('pass the value: ' + String(INPUT), async (t) => {
+        t.plan(2);
+
+        const result = unwrapOrElseAsyncFromMaybe(INPUT, () => {
+            t.fail('should not call recover fn');
+            return Math.random();
+        });
+
+        t.true(result instanceof Promise, 'result should be Promise');
+        const actual = await result;
+        t.is(actual, EXPECTED);
+    });
+}
+
+for (const NULL_VALUE of [undefined, null]) {
+    test(`pass ${NULL_VALUE}`, async (t) => {
+        t.plan(3);
+
+        const EXPECTED = Math.random();
+        const result = unwrapOrElseAsyncFromMaybe(NULL_VALUE, async () => {
+            t.pass('should call recover fn');
+            return EXPECTED;
+        });
+
+        t.true(result instanceof Promise, 'result should be Promise');
+        const actual = await result;
+        t.is(actual, EXPECTED);
+    });
+
+    for (const FALLBACK_VALUE of [null, undefined]) {
+        test(`should not accept Maybe<*> as default, v = ${String(NULL_VALUE)}, def = ${String(
+            FALLBACK_VALUE
+        )}`, async (t) => {
+            t.plan(2);
+
+            await t.throwsAsync(
+                async () => {
+                    await unwrapOrElseAsyncFromMaybe(NULL_VALUE, async () => {
+                        t.pass('should be called');
+                        return FALLBACK_VALUE;
+                    });
+                },
+                {
+                    instanceOf: TypeError,
+                    message: '`recoverer` must not return `null` or `undefined`',
+                }
+            );
+        });
+    }
+
+    test(`input is ${NULL_VALUE}: callback should return Promise`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                await unwrapOrElseAsyncFromMaybe(NULL_VALUE, () => {
+                    t.pass();
+                    return 1;
+                });
+            },
+            {
+                instanceOf: TypeError,
+                message: '`recoverer` must return Promise',
+            }
+        );
+    });
+}