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

Issue:
Implement mapOrElseAsync for Maybe

Pull request:
Implement mapOrElseAsync for Maybe 
Fix #934

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 14fe93c2..f1189eda 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -21,6 +21,7 @@
 - [option-t/Maybe/Maybe](../src/Maybe/Maybe.ts)
 - [option-t/Maybe/mapOr](../src/Maybe/mapOr.ts)
 - [option-t/Maybe/mapOrElse](../src/Maybe/mapOrElse.ts)
+- [option-t/Maybe/mapOrElseAsync](../src/Maybe/mapOrElseAsync.ts)
 - [option-t/Maybe/or](../src/Maybe/or.ts)
 - [option-t/Maybe/orElse](../src/Maybe/orElse.ts)
 - [option-t/Maybe/orElseAsync](../src/Maybe/orElseAsync.ts)
diff --git a/src/Maybe/index.ts b/src/Maybe/index.ts
index 38b7b8c4..37ccc104 100644
--- a/src/Maybe/index.ts
+++ b/src/Maybe/index.ts
@@ -8,6 +8,7 @@ export { mapForMaybe as map } from './map';
 export { mapAsyncForMaybe as mapAsync } from './mapAsync';
 export { mapOrForMaybe as mapOr } from './mapOr';
 export { mapOrElseForMaybe as mapOrElse } from './mapOrElse';
+export { mapOrElseAsyncForMaybe as mapOrElseAsync } from './mapOrElseAsync';
 // XXX: `or()` operation is equivalent of `a || b` so we don't ship it by default set.
 // export { orForMaybe as or } from './or';
 export { orElseForMaybe as orElse } from './orElse';
diff --git a/src/Maybe/mapOrElseAsync.ts b/src/Maybe/mapOrElseAsync.ts
new file mode 100644
index 00000000..a91224bd
--- /dev/null
+++ b/src/Maybe/mapOrElseAsync.ts
@@ -0,0 +1,55 @@
+import { assertIsPromise } from '../internal/assert';
+import {
+    ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE,
+    ERR_MSG_RECOVERER_MUST_RETURN_PROMISE,
+} from '../internal/ErrorMessage';
+import type { AsyncTransformFn, AsyncRecoveryFn } from '../internal/Function';
+
+import {
+    ERR_MSG_TRANSFORMER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE,
+    ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE,
+} from './ErrorMessage';
+import { Maybe, isNotNullAndUndefined } from './Maybe';
+import { expectNotNullAndUndefined } from './expect';
+
+/**
+ *  Return the result of _transformer_ with using _input_ as an argument for it if _input_ is not `null` and `undefined`.
+ *  Otherwise, return the result of _recoverer_.
+ *
+ *  Basically, this operation is a combination `map()` and `unwrapOrElse()`.
+ *
+ *  * `U` must not be `Maybe<*>`.
+ *      * If the result of _transformer_ is `null` or `undefined`, this throw an `Error`.
+ *      * If the result of _recoverer_ is `null` or `undefined`, this throw an `Error`.
+ *  * If you'd like to accept `Maybe<*>` as `U`, use a combination `andThen()` and `orElse()`.
+ */
+export function mapOrElseAsyncForMaybe<T, U>(
+    input: Maybe<T>,
+    recoverer: AsyncRecoveryFn<U>,
+    transformer: AsyncTransformFn<T, U>
+): Promise<U> {
+    let result: Promise<U>;
+    let messageForPromiseCheck = '';
+    let messageForExpect = '';
+
+    if (isNotNullAndUndefined(input)) {
+        result = transformer(input);
+        messageForPromiseCheck = ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE;
+        messageForExpect = ERR_MSG_TRANSFORMER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE;
+    } else {
+        result = recoverer();
+        messageForPromiseCheck = ERR_MSG_RECOVERER_MUST_RETURN_PROMISE;
+        messageForExpect = ERR_MSG_RECOVERER_MUST_NOT_RETURN_NO_VAL_FOR_MAYBE;
+    }
+
+    // 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 = expectNotNullAndUndefined(result, messageForExpect);
+        return unwrappedResult;
+    });
+    return passed;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 7e4220ae..91692bd1 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -73,6 +73,8 @@
     "cjs/Maybe/mapOr.js": null,
     "cjs/Maybe/mapOrElse.d.ts": null,
     "cjs/Maybe/mapOrElse.js": null,
+    "cjs/Maybe/mapOrElseAsync.d.ts": null,
+    "cjs/Maybe/mapOrElseAsync.js": null,
     "cjs/Maybe/or.d.ts": null,
     "cjs/Maybe/or.js": null,
     "cjs/Maybe/orElse.d.ts": null,
@@ -365,6 +367,8 @@
     "esm/Maybe/mapOr.mjs": null,
     "esm/Maybe/mapOrElse.d.ts": null,
     "esm/Maybe/mapOrElse.mjs": null,
+    "esm/Maybe/mapOrElseAsync.d.ts": null,
+    "esm/Maybe/mapOrElseAsync.mjs": null,
     "esm/Maybe/or.d.ts": null,
     "esm/Maybe/or.mjs": null,
     "esm/Maybe/orElse.d.ts": null,
@@ -688,6 +692,9 @@
     "lib/Maybe/mapOrElse.d.ts": null,
     "lib/Maybe/mapOrElse.js": null,
     "lib/Maybe/mapOrElse.mjs": null,
+    "lib/Maybe/mapOrElseAsync.d.ts": null,
+    "lib/Maybe/mapOrElseAsync.js": null,
+    "lib/Maybe/mapOrElseAsync.mjs": null,
     "lib/Maybe/or.d.ts": null,
     "lib/Maybe/or.js": null,
     "lib/Maybe/or.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index 96c96dcd..439a7465 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -16,6 +16,7 @@ export default {
             'mapAsync',
             'mapOr',
             'mapOrElse',
+            'mapOrElseAsync',
             'orElse',
             'orElseAsync',
             'tap',
@@ -71,6 +72,11 @@ export default {
             'mapOrElseForMaybe'
         ]
     },
+    'Maybe/mapOrElseAsync': {
+        'exports': [
+            'mapOrElseAsyncForMaybe'
+        ]
+    },
     'Maybe/or': {
         'exports': [
             'orForMaybe'


Test patch:
diff --git a/__tests__/Maybe/map_or_else.test.mjs b/__tests__/Maybe/map_or_else.test.mjs
index 184f16ed..d8639a1d 100644
--- a/__tests__/Maybe/map_or_else.test.mjs
+++ b/__tests__/Maybe/map_or_else.test.mjs
@@ -3,7 +3,7 @@ import test from 'ava';
 import { mapOrElseForMaybe } from '../../__dist/esm/Maybe/mapOrElse.mjs';
 import { nonNullableValueCaseListForSync } from '../utils.mjs';
 
-for (const [INPUT, , EXPECTED] of nonNullableValueCaseListForSync) {
+for (const [INPUT, PASSED_VALUE, EXPECTED] of nonNullableValueCaseListForSync) {
     test('value:' + String(INPUT), (t) => {
         t.plan(3);
 
@@ -17,7 +17,7 @@ for (const [INPUT, , EXPECTED] of nonNullableValueCaseListForSync) {
             },
             (v) => {
                 t.pass('should call selector fn');
-                t.is(v, EXPECTED, 'the arg is the input');
+                t.is(v, PASSED_VALUE, 'the arg is the input');
                 return v;
             }
         );
@@ -46,53 +46,49 @@ for (const NULL_VALUE of [undefined, null]) {
         );
         t.is(result, DEFAULE_VAL);
     });
-}
 
-{
-    const testcases = [
-        [1, 2, undefined],
-        [1, 2, null],
-    ];
-    for (const [src, def, selectorResult] of testcases) {
-        test(`assert that do not return Maybe<*> as the selector's result. v = ${String(
-            src
-        )}, def = ${String(def)}, selectorResult=${String(selectorResult)}`, (t) => {
-            t.plan(1);
-            t.throws(
-                () => {
-                    mapOrElseForMaybe(
-                        src,
-                        () => def,
-                        (_v) => selectorResult
-                    );
-                },
-                {
-                    instanceOf: TypeError,
-                    message: '`transformer` must not return `null` or `undefined`',
-                }
-            );
-        });
-    }
-}
+    test(`assert that do not return Maybe<*> as the transformer's result. transformer's result=${String(
+        NULL_VALUE
+    )}`, (t) => {
+        t.plan(2);
+
+        t.throws(
+            () => {
+                const INPUT = Math.random();
+                mapOrElseForMaybe(
+                    INPUT,
+                    () => {
+                        t.fail('do not call this');
+                        return Math.random();
+                    },
+                    (_v) => {
+                        t.pass('call here');
+                        return NULL_VALUE;
+                    }
+                );
+            },
+            {
+                instanceOf: TypeError,
+                message: '`transformer` must not return `null` or `undefined`',
+            }
+        );
+    });
 
-{
-    const testcases = [
-        [null, undefined, ''],
-        [null, null, ''],
-        [undefined, undefined, ''],
-        [undefined, null, ''],
-    ];
-    for (const [src, def, selectorResult] of testcases) {
-        test(`v = ${String(src)}, def = ${String(def)}, selectorResult=${String(
-            selectorResult
-        )}`, (t) => {
-            t.plan(1);
+    for (const FALLBACK_VALUE of [undefined, null]) {
+        test(`v = ${String(NULL_VALUE)}, def = ${String(FALLBACK_VALUE)}`, (t) => {
+            t.plan(2);
             t.throws(
                 () => {
                     mapOrElseForMaybe(
-                        src,
-                        () => def,
-                        (_v) => selectorResult
+                        NULL_VALUE,
+                        () => {
+                            t.pass('call this');
+                            return FALLBACK_VALUE;
+                        },
+                        (_v) => {
+                            t.fail('do not call this');
+                            return Math.random();
+                        }
                     );
                 },
                 {
diff --git a/__tests__/Maybe/map_or_else_async.test.mjs b/__tests__/Maybe/map_or_else_async.test.mjs
new file mode 100644
index 00000000..c2ed01ee
--- /dev/null
+++ b/__tests__/Maybe/map_or_else_async.test.mjs
@@ -0,0 +1,157 @@
+import test from 'ava';
+
+import { mapOrElseAsyncForMaybe } from '../../__dist/esm/Maybe/mapOrElseAsync.mjs';
+import { nonNullableValueCaseListForAsync } from '../utils.mjs';
+
+for (const [INPUT, PASSED_VALUE, EXPECTED] of nonNullableValueCaseListForAsync) {
+    test('value:' + String(INPUT), async (t) => {
+        t.plan(4);
+
+        const DEFAULT_VAL = Symbol('def');
+
+        const result = mapOrElseAsyncForMaybe(
+            INPUT,
+            async () => {
+                t.fail('do not call here');
+                return DEFAULT_VAL;
+            },
+            async (v) => {
+                t.pass('should call selector fn');
+                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);
+    });
+}
+
+for (const NULL_VALUE of [undefined, null]) {
+    test(`pass ${NULL_VALUE}`, async (t) => {
+        t.plan(3);
+
+        const DEFAULE_VAL = Symbol('default');
+        const COMPUTED_VAL = Symbol('computed');
+
+        const result = mapOrElseAsyncForMaybe(
+            NULL_VALUE,
+            async () => {
+                t.pass('should be called');
+                return DEFAULE_VAL;
+            },
+            async (_v) => {
+                t.fail('should not call selector fn');
+                return COMPUTED_VAL;
+            }
+        );
+
+        t.true(result instanceof Promise, 'result should be Promise');
+        const actual = await result;
+        t.is(actual, DEFAULE_VAL);
+    });
+
+    test(`assert that do not return Maybe<*> as the transformer's result. transformer's result=${String(
+        NULL_VALUE
+    )}`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                const INPUT = Math.random();
+                await mapOrElseAsyncForMaybe(
+                    INPUT,
+                    async () => {
+                        t.fail('do not call this');
+                        return Math.random();
+                    },
+                    async (_v) => {
+                        t.pass('call here');
+                        return NULL_VALUE;
+                    }
+                );
+            },
+            {
+                instanceOf: TypeError,
+                message: '`transformer` must not return `null` or `undefined`',
+            }
+        );
+    });
+
+    for (const FALLBACK_VALUE of [undefined, null]) {
+        test(`v = ${String(NULL_VALUE)}, def = ${String(FALLBACK_VALUE)}`, async (t) => {
+            t.plan(2);
+            await t.throwsAsync(
+                async () => {
+                    await mapOrElseAsyncForMaybe(
+                        NULL_VALUE,
+                        async () => {
+                            t.pass('call this');
+                            return FALLBACK_VALUE;
+                        },
+                        async (_v) => {
+                            t.fail('do not call this');
+                            return Math.random();
+                        }
+                    );
+                },
+                {
+                    instanceOf: TypeError,
+                    message: '`recoverer` must not return `null` or `undefined`',
+                }
+            );
+        });
+    }
+
+    test(`${NULL_VALUE}: transformer should return Promise`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                const INPUT = Math.random();
+                await mapOrElseAsyncForMaybe(
+                    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(`${NULL_VALUE}: recoverer should return Promise`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                await mapOrElseAsyncForMaybe(
+                    NULL_VALUE,
+                    () => {
+                        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',
+            }
+        );
+    });
+}