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

Issue:
Implement unwrapOrElseAsync for Undefinable

Pull request description:
Implement unwrapOrElseAsync() for Undefinable
Fix #942

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 58b2d54c..f46e45ba 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -132,5 +132,6 @@
 - [option-t/Undefinable/unwrap](../src/Undefinable/unwrap.ts)
 - [option-t/Undefinable/unwrapOr](../src/Undefinable/unwrapOr.ts)
 - [option-t/Undefinable/unwrapOrElse](../src/Undefinable/unwrapOrElse.ts)
+- [option-t/Undefinable/unwrapOrElseAsync](../src/Undefinable/unwrapOrElseAsync.ts)
 - [option-t/Undefinable/xor](../src/Undefinable/xor.ts)
 
diff --git a/src/Undefinable/index.ts b/src/Undefinable/index.ts
index 3b117616..80c52ed6 100644
--- a/src/Undefinable/index.ts
+++ b/src/Undefinable/index.ts
@@ -16,3 +16,4 @@ export { tapUndefinable as tap } from './tap';
 export { unwrapUndefinable as unwrap } from './unwrap';
 export { unwrapOrFromUndefinable as unwrapOr } from './unwrapOr';
 export { unwrapOrElseFromUndefinable as unwrapOrElse } from './unwrapOrElse';
+export { unwrapOrElseAsyncFromUndefinable as unwrapOrElseAsync } from './unwrapOrElseAsync';
diff --git a/src/Undefinable/unwrapOrElseAsync.ts b/src/Undefinable/unwrapOrElseAsync.ts
new file mode 100644
index 00000000..d11996d5
--- /dev/null
+++ b/src/Undefinable/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 { Undefinable, isNotUndefined } from './Undefinable';
+import { expectNotUndefined } from './expect';
+import { ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE } from './ErrorMessage';
+
+function check<T>(value: Undefinable<T>): T {
+    const passed = expectNotUndefined(
+        value,
+        ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE
+    );
+    return passed;
+}
+
+/**
+ *  Return _input_ as `T` if the passed _input_ is not `undefined`.
+ *  Otherwise, return the result of _recoverer_.
+ *
+ *  * The result of _recoverer_ must not be `Undefinable<*>`.
+ *      * If you try to recover the value, use `orElse()`
+ *  * If the result of _recoverer_ is `undefined`, throw `TypeError`.
+ */
+export function unwrapOrElseAsyncFromUndefinable<T>(
+    input: Undefinable<T>,
+    recoverer: AsyncRecoveryFn<T>
+): Promise<T> {
+    if (isNotUndefined(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 ecdd94f6..68a1287c 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -271,6 +271,8 @@
     "cjs/Undefinable/unwrapOr.js": null,
     "cjs/Undefinable/unwrapOrElse.d.ts": null,
     "cjs/Undefinable/unwrapOrElse.js": null,
+    "cjs/Undefinable/unwrapOrElseAsync.d.ts": null,
+    "cjs/Undefinable/unwrapOrElseAsync.js": null,
     "cjs/Undefinable/xor.d.ts": null,
     "cjs/Undefinable/xor.js": null,
     "cjs/index.d.ts": null,
@@ -551,6 +553,8 @@
     "esm/Undefinable/unwrapOr.mjs": null,
     "esm/Undefinable/unwrapOrElse.d.ts": null,
     "esm/Undefinable/unwrapOrElse.mjs": null,
+    "esm/Undefinable/unwrapOrElseAsync.d.ts": null,
+    "esm/Undefinable/unwrapOrElseAsync.mjs": null,
     "esm/Undefinable/xor.d.ts": null,
     "esm/Undefinable/xor.mjs": null,
     "esm/index.d.ts": null,
@@ -961,6 +965,9 @@
     "lib/Undefinable/unwrapOrElse.d.ts": null,
     "lib/Undefinable/unwrapOrElse.js": null,
     "lib/Undefinable/unwrapOrElse.mjs": null,
+    "lib/Undefinable/unwrapOrElseAsync.d.ts": null,
+    "lib/Undefinable/unwrapOrElseAsync.js": null,
+    "lib/Undefinable/unwrapOrElseAsync.mjs": null,
     "lib/Undefinable/xor.d.ts": null,
     "lib/Undefinable/xor.js": null,
     "lib/Undefinable/xor.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index bb35fcda..58cdff0b 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -568,6 +568,7 @@ export default {
             'unwrap',
             'unwrapOr',
             'unwrapOrElse',
+            'unwrapOrElseAsync',
         ]
     },
 
@@ -652,6 +653,11 @@ export default {
             'unwrapOrElseFromUndefinable'
         ]
     },
+    'Undefinable/unwrapOrElseAsync': {
+        'exports': [
+            'unwrapOrElseAsyncFromUndefinable'
+        ]
+    },
     'Undefinable/xor': {
         'exports': [
             'xorForUndefinable'


Test patch:
diff --git a/__tests__/Undefinable/unwrap_or_else_async.test.mjs b/__tests__/Undefinable/unwrap_or_else_async.test.mjs
new file mode 100644
index 00000000..1489f366
--- /dev/null
+++ b/__tests__/Undefinable/unwrap_or_else_async.test.mjs
@@ -0,0 +1,103 @@
+import test from 'ava';
+
+import { unwrapOrElseAsyncFromUndefinable } from '../../__dist/esm/Undefinable/unwrapOrElseAsync.mjs';
+import { nonNullableValue } from '../utils.mjs';
+
+const NULL_VALUE_IN_THIS_TEST_CASE = undefined;
+const NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE = null;
+
+const nonNullableValueCaseList = nonNullableValue.map((input) => {
+    if (input instanceof Promise) {
+        // Promise will be unwrap recursively automatically.
+        // There is no ways to unwrap single level now.
+        // So we need to care that.
+        return [input, 'this is promise'];
+    }
+
+    return [input, input];
+});
+
+for (const [INPUT, EXPECTED] of nonNullableValueCaseList) {
+    test('pass the value: ' + String(INPUT), async (t) => {
+        t.plan(3);
+
+        const DEFAULT_VAL = Math.random();
+
+        let result;
+        t.notThrows(() => {
+            result = unwrapOrElseAsyncFromUndefinable(INPUT, () => {
+                t.fail('should not call recover fn');
+                return DEFAULT_VAL;
+            });
+        });
+
+        t.true(result instanceof Promise, 'result should be Promise');
+
+        const actual = await result;
+        t.is(actual, EXPECTED);
+    });
+}
+
+test(`pass ${NULL_VALUE_IN_THIS_TEST_CASE}`, async (t) => {
+    t.plan(3);
+
+    const DEFAULT_VAL = Math.random();
+    const result = unwrapOrElseAsyncFromUndefinable(NULL_VALUE_IN_THIS_TEST_CASE, async () => {
+        t.pass('should call recover fn');
+        return DEFAULT_VAL;
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, DEFAULT_VAL);
+});
+
+test(`pass ${NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE}`, async (t) => {
+    t.plan(2);
+
+    const DEFAULT_VAL = Math.random();
+    const result = unwrapOrElseAsyncFromUndefinable(
+        NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE,
+        async () => {
+            t.fail('should not call recover fn');
+            return DEFAULT_VAL;
+        }
+    );
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE);
+});
+
+const testcases = [[NULL_VALUE_IN_THIS_TEST_CASE, NULL_VALUE_IN_THIS_TEST_CASE]];
+for (const [src, def] of testcases) {
+    test(`should not accept null as default value, v = ${String(src)}, def = ${String(
+        def
+    )}`, async (t) => {
+        await t.throwsAsync(
+            async () => {
+                await unwrapOrElseAsyncFromUndefinable(src, async () => def);
+            },
+            { instanceOf: TypeError, message: '`recoverer` must not return `undefined`' }
+        );
+    });
+}
+
+test('callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            await unwrapOrElseAsyncFromUndefinable(NULL_VALUE_IN_THIS_TEST_CASE, () => {
+                t.pass();
+                return 1;
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: '`recoverer` must return Promise',
+        }
+    );
+});