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

Issue:
Implement orElseAsync for Maybe

Pull request:
Implement orElseAsync for Maybe
Fix #935

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index ed7e1e16..0e612bf1 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -23,6 +23,7 @@
 - [option-t/Maybe/mapOrElse](../src/Maybe/mapOrElse.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)
 - [option-t/Maybe/tap](../src/Maybe/tap.ts)
 - [option-t/Maybe/unwrap](../src/Maybe/unwrap.ts)
 - [option-t/Maybe/unwrapOr](../src/Maybe/unwrapOr.ts)
diff --git a/src/Maybe/index.ts b/src/Maybe/index.ts
index 1947b20d..90900247 100644
--- a/src/Maybe/index.ts
+++ b/src/Maybe/index.ts
@@ -11,6 +11,7 @@ export { mapOrElseForMaybe as mapOrElse } from './mapOrElse';
 // 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';
+export { orElseAsyncForMaybe as orElseAsync } from './orElseAsync';
 export { tapMaybe as tap } from './tap';
 export { unwrapMaybe as unwrap } from './unwrap';
 export { unwrapOrFromMaybe as unwrapOr } from './unwrapOr';
diff --git a/src/Maybe/orElseAsync.ts b/src/Maybe/orElseAsync.ts
new file mode 100644
index 00000000..6118f78f
--- /dev/null
+++ b/src/Maybe/orElseAsync.ts
@@ -0,0 +1,29 @@
+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';
+
+export type MaybeAsyncRecoveryFn<T> = AsyncRecoveryFn<Maybe<T>>;
+
+/**
+ *  Return _input_ as `T` if the passed _input_ is not `null` and `undefined`.
+ *  Otherwise, return the result of _recoverer_.
+ */
+export function orElseAsyncForMaybe<T>(
+    input: Maybe<T>,
+    recoverer: MaybeAsyncRecoveryFn<T>
+): Promise<Maybe<T>> {
+    if (isNotNullAndUndefined(input)) {
+        return Promise.resolve(input);
+    }
+
+    const fallback = 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);
+
+    return fallback;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 8b5ac56b..318bfeee 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -77,6 +77,8 @@
     "cjs/Maybe/or.js": null,
     "cjs/Maybe/orElse.d.ts": null,
     "cjs/Maybe/orElse.js": null,
+    "cjs/Maybe/orElseAsync.d.ts": null,
+    "cjs/Maybe/orElseAsync.js": null,
     "cjs/Maybe/tap.d.ts": null,
     "cjs/Maybe/tap.js": null,
     "cjs/Maybe/unwrap.d.ts": null,
@@ -365,6 +367,8 @@
     "esm/Maybe/or.mjs": null,
     "esm/Maybe/orElse.d.ts": null,
     "esm/Maybe/orElse.mjs": null,
+    "esm/Maybe/orElseAsync.d.ts": null,
+    "esm/Maybe/orElseAsync.mjs": null,
     "esm/Maybe/tap.d.ts": null,
     "esm/Maybe/tap.mjs": null,
     "esm/Maybe/unwrap.d.ts": null,
@@ -686,6 +690,9 @@
     "lib/Maybe/orElse.d.ts": null,
     "lib/Maybe/orElse.js": null,
     "lib/Maybe/orElse.mjs": null,
+    "lib/Maybe/orElseAsync.d.ts": null,
+    "lib/Maybe/orElseAsync.js": null,
+    "lib/Maybe/orElseAsync.mjs": null,
     "lib/Maybe/tap.d.ts": null,
     "lib/Maybe/tap.js": null,
     "lib/Maybe/tap.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index 3c953b6e..a77eab39 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -17,6 +17,7 @@ export default {
             'mapOr',
             'mapOrElse',
             'orElse',
+            'orElseAsync',
             'tap',
             'unwrap',
             'unwrapOr',
@@ -79,6 +80,11 @@ export default {
             'orElseForMaybe'
         ]
     },
+    'Maybe/orElseAsync': {
+        'exports': [
+            'orElseAsyncForMaybe'
+        ]
+    },
     'Maybe/tap': {
         'exports': [
             'tapMaybe'


Test patch:
diff --git a/__tests__/Maybe/or_else_async.test.mjs b/__tests__/Maybe/or_else_async.test.mjs
new file mode 100644
index 00000000..99367281
--- /dev/null
+++ b/__tests__/Maybe/or_else_async.test.mjs
@@ -0,0 +1,64 @@
+import test from 'ava';
+
+import { orElseAsyncForMaybe } from '../../__dist/esm/Maybe/orElseAsync.mjs';
+import { nonNullableValue } from '../utils.mjs';
+
+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(2);
+
+        const DEFAULT_VAL = Math.random();
+        const result = orElseAsyncForMaybe(INPUT, async () => {
+            t.fail('should not call');
+            return DEFAULT_VAL;
+        });
+
+        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 DEFAULT_VAL = Math.random();
+        const result = orElseAsyncForMaybe(NULL_VALUE, 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(`input is ${NULL_VALUE}, callback should return Promise`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                await orElseAsyncForMaybe(NULL_VALUE, () => {
+                    t.pass('should be called');
+                    return 1;
+                });
+            },
+            {
+                instanceOf: TypeError,
+                message: '`recoverer` must return Promise',
+            }
+        );
+    });
+}