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

Issue:
Implement mapOrElseAsync for Undefinable

Pull request description:
Implement mapOrElseAsync for Undefinable
Fix #940

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 724de9bc..410f6c70 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -125,6 +125,7 @@
 - [option-t/Undefinable/mapOr](../src/Undefinable/mapOr.ts)
 - [option-t/Undefinable/mapOrAsync](../src/Undefinable/mapOrAsync.ts)
 - [option-t/Undefinable/mapOrElse](../src/Undefinable/mapOrElse.ts)
+- [option-t/Undefinable/mapOrElseAsync](../src/Undefinable/mapOrElseAsync.ts)
 - [option-t/Undefinable/or](../src/Undefinable/or.ts)
 - [option-t/Undefinable/orElse](../src/Undefinable/orElse.ts)
 - [option-t/Undefinable/orElseAsync](../src/Undefinable/orElseAsync.ts)
diff --git a/src/Undefinable/index.ts b/src/Undefinable/index.ts
index ac7ac9c2..33e1f587 100644
--- a/src/Undefinable/index.ts
+++ b/src/Undefinable/index.ts
@@ -9,6 +9,7 @@ export { mapAsyncForUndefinable as mapAsync } from './mapAsync';
 export { mapOrForUndefinable as mapOr } from './mapOr';
 export { mapOrAsyncForUndefinable as mapOrAsync } from './mapOrAsync';
 export { mapOrElseForUndefinable as mapOrElse } from './mapOrElse';
+export { mapOrElseAsyncForUndefinable as mapOrElseAsync } from './mapOrElseAsync';
 // XXX: `or()` operation is equivalent of `a || b` so we don't ship it by default set.
 // export { orForUndefinable as or} from './or';
 export { orElseForUndefinable as orElse } from './orElse';
diff --git a/src/Undefinable/mapOrElseAsync.ts b/src/Undefinable/mapOrElseAsync.ts
new file mode 100644
index 00000000..1a2036d8
--- /dev/null
+++ b/src/Undefinable/mapOrElseAsync.ts
@@ -0,0 +1,54 @@
+import type { AsyncTransformFn, AsyncRecoveryFn } from '../internal/Function';
+import { Undefinable, isNotUndefined } from './Undefinable';
+import { assertIsPromise } from '../internal/assert';
+import {
+    ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE,
+    ERR_MSG_RECOVERER_MUST_RETURN_PROMISE,
+} from '../internal/ErrorMessage';
+import {
+    ERR_MSG_TRANSFORMER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE,
+    ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE,
+} from './ErrorMessage';
+import { expectNotUndefined } from './expect';
+
+/**
+ *  Return the result of _transformer_ with using _input_ as an argument for it if _input_ is not `undefined`.
+ *  Otherwise, return the result of _recoverer_.
+ *
+ *  Basically, this operation is a combination `map()` and `unwrapOrElse()`.
+ *
+ *  * `U` must not be `Undefinable<*>`.
+ *      * If the result of _transformer_ is `undefined`, this throw an `Error`.
+ *      * If the result of _recoverer_ is undefined`, this throw an `Error`.
+ *  * If you'd like to accept `Undefinable<*>` as `U`, use a combination `andThen()` and `orElse()`.
+ */
+export function mapOrElseAsyncForUndefinable<T, U>(
+    input: Undefinable<T>,
+    recoverer: AsyncRecoveryFn<U>,
+    transformer: AsyncTransformFn<T, U>
+): Promise<U> {
+    let result: Promise<U>;
+    let messageForPromiseCheck = '';
+    let messageForExpect = '';
+
+    if (isNotUndefined(input)) {
+        result = transformer(input);
+        messageForPromiseCheck = ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE;
+        messageForExpect = ERR_MSG_TRANSFORMER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE;
+    } else {
+        result = recoverer();
+        messageForPromiseCheck = ERR_MSG_RECOVERER_MUST_RETURN_PROMISE;
+        messageForExpect = ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_UNDEFINABLE;
+    }
+
+    // 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(result, messageForPromiseCheck);
+
+    const passed = result.then((result) => {
+        const unwrappedResult = expectNotUndefined(result, messageForExpect);
+        return unwrappedResult;
+    });
+    return passed;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 326dacd1..1f7c81fc 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -259,6 +259,8 @@
     "cjs/Undefinable/mapOrAsync.js": null,
     "cjs/Undefinable/mapOrElse.d.ts": null,
     "cjs/Undefinable/mapOrElse.js": null,
+    "cjs/Undefinable/mapOrElseAsync.d.ts": null,
+    "cjs/Undefinable/mapOrElseAsync.js": null,
     "cjs/Undefinable/or.d.ts": null,
     "cjs/Undefinable/or.js": null,
     "cjs/Undefinable/orElse.d.ts": null,
@@ -543,6 +545,8 @@
     "esm/Undefinable/mapOrAsync.mjs": null,
     "esm/Undefinable/mapOrElse.d.ts": null,
     "esm/Undefinable/mapOrElse.mjs": null,
+    "esm/Undefinable/mapOrElseAsync.d.ts": null,
+    "esm/Undefinable/mapOrElseAsync.mjs": null,
     "esm/Undefinable/or.d.ts": null,
     "esm/Undefinable/or.mjs": null,
     "esm/Undefinable/orElse.d.ts": null,
@@ -951,6 +955,9 @@
     "lib/Undefinable/mapOrElse.d.ts": null,
     "lib/Undefinable/mapOrElse.js": null,
     "lib/Undefinable/mapOrElse.mjs": null,
+    "lib/Undefinable/mapOrElseAsync.d.ts": null,
+    "lib/Undefinable/mapOrElseAsync.js": null,
+    "lib/Undefinable/mapOrElseAsync.mjs": null,
     "lib/Undefinable/or.d.ts": null,
     "lib/Undefinable/or.js": null,
     "lib/Undefinable/or.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index 938f994d..21ce5dda 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -563,6 +563,7 @@ export default {
             'mapOr',
             'mapOrAsync',
             'mapOrElse',
+            'mapOrElseAsync',
             'orElse',
             'orElseAsync',
             'tap',
@@ -618,6 +619,11 @@ export default {
             'mapOrElseForUndefinable'
         ]
     },
+    'Undefinable/mapOrElseAsync': {
+        'exports': [
+            'mapOrElseAsyncForUndefinable'
+        ]
+    },
     'Undefinable/or': {
         'exports': [
             'orForUndefinable'


Test patch:
diff --git a/__tests__/Undefinable/map_or_else_async.test.mjs b/__tests__/Undefinable/map_or_else_async.test.mjs
new file mode 100644
index 00000000..f3b52c35
--- /dev/null
+++ b/__tests__/Undefinable/map_or_else_async.test.mjs
@@ -0,0 +1,202 @@
+import test from 'ava';
+
+import { mapOrElseAsyncForUndefinable } from '../../__dist/esm/Undefinable/mapOrElseAsync.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, input, 'this is promise'];
+    }
+
+    return [input, input, input];
+});
+
+for (const [INPUT, PASSED_VALUE, EXPECTED] of nonNullableValueCaseList) {
+    test('pass the value: ' + String(INPUT), async (t) => {
+        t.plan(3);
+
+        const DEFAULT_VAL = Symbol('def');
+
+        const result = mapOrElseAsyncForUndefinable(
+            INPUT,
+            async () => {
+                t.fail('do not call this');
+                return DEFAULT_VAL;
+            },
+            async (v) => {
+                t.is(v, PASSED_VALUE, 'the arg is the input');
+                return v;
+            }
+        );
+
+        t.true(result instanceof Promise, 'result should be Promise');
+        const actual = await result;
+        t.is(actual, EXPECTED);
+    });
+}
+
+test('pass null', async (t) => {
+    t.plan(3);
+
+    const DEFAULE_VAL = Symbol('');
+    const COMPUTED_VAL = Symbol('');
+    const result = mapOrElseAsyncForUndefinable(
+        NULL_VALUE_IN_THIS_TEST_CASE,
+        async () => {
+            t.pass('call this path');
+            return DEFAULE_VAL;
+        },
+        async (_v) => {
+            t.fail('do not call this');
+            return COMPUTED_VAL;
+        }
+    );
+
+    t.true(result instanceof Promise, 'result should be Promise');
+    const actual = await result;
+    t.is(actual, DEFAULE_VAL);
+});
+
+test('pass undefined', async (t) => {
+    t.plan(3);
+
+    const DEFAULE_VAL = Symbol('');
+    const COMPUTED_VAL = Symbol('');
+
+    const result = mapOrElseAsyncForUndefinable(
+        NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE,
+        async () => {
+            t.fail('do not call this');
+            return DEFAULE_VAL;
+        },
+        async (v) => {
+            t.is(v, NULLY_VALUE_BUT_NOT_NULL_VALUE_IN_THIS_TEST_CASE);
+            return COMPUTED_VAL;
+        }
+    );
+
+    t.true(result instanceof Promise, 'result should be Promise');
+    const actual = await result;
+    t.is(actual, COMPUTED_VAL);
+});
+
+{
+    const testcases = [[1, 2, NULL_VALUE_IN_THIS_TEST_CASE]];
+    for (const [src, def, selectorResult] of testcases) {
+        test(`assert that do not return Nullable<*> as the selector's result: v = ${String(
+            src
+        )}, def = ${String(def)}, selectorResult=${String(selectorResult)}`, async (t) => {
+            t.plan(2);
+
+            await t.throwsAsync(
+                async () => {
+                    await mapOrElseAsyncForUndefinable(
+                        src,
+                        async () => {
+                            t.fail('do not call this pass to here');
+                            return def;
+                        },
+                        async (_v) => {
+                            t.pass('this pass should be called');
+                            return selectorResult;
+                        }
+                    );
+                    t.fail('do not reach here');
+                },
+                {
+                    instanceOf: TypeError,
+                    message: '`transformer` must not return `undefined`',
+                }
+            );
+        });
+    }
+}
+
+{
+    const testcases = [[NULL_VALUE_IN_THIS_TEST_CASE, NULL_VALUE_IN_THIS_TEST_CASE, '']];
+    for (const [src, def, selectorResult] of testcases) {
+        test(`assert that def is not Nullable<*>: v = ${String(src)}, def = ${String(
+            def
+        )}, selectorResult=${String(selectorResult)}`, async (t) => {
+            t.plan(2);
+
+            await t.throwsAsync(
+                async () => {
+                    await mapOrElseAsyncForUndefinable(
+                        src,
+                        async () => {
+                            t.pass('this pass should be called');
+                            return def;
+                        },
+                        async (_v) => {
+                            t.fail('do not call this pass to here');
+                            return Math.random();
+                        }
+                    );
+                    t.fail('do not reach here');
+                },
+                {
+                    instanceOf: TypeError,
+                    message: '`recoverer` must not return `undefined`',
+                }
+            );
+        });
+    }
+}
+
+test('transformer should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const INPUT = Math.random();
+            await mapOrElseAsyncForUndefinable(
+                INPUT,
+                () => {
+                    t.fail('do not call this');
+                    return Math.random();
+                },
+                () => {
+                    t.pass('this should be called');
+                    return Math.random();
+                }
+            );
+            t.fail('do not reach here');
+        },
+        {
+            instanceOf: TypeError,
+            message: '`transformer` must return Promise',
+        }
+    );
+});
+
+test('recoverer should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            await mapOrElseAsyncForUndefinable(
+                NULL_VALUE_IN_THIS_TEST_CASE,
+                () => {
+                    t.pass('this should be called');
+                    return Math.random();
+                },
+                () => {
+                    t.fail('do not call this');
+                    return Math.random();
+                }
+            );
+            t.fail('do not reach here');
+        },
+        {
+            instanceOf: TypeError,
+            message: '`recoverer` must return Promise',
+        }
+    );
+});