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

Issue:
Implement mapOrElseAsync for PlainOption

Pull request description:
Implement `mapOrElseAsync()` for PlainOption
Fix #928

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 414b1977..a81de4e8 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -75,6 +75,7 @@
 - [option-t/PlainOption/mapAsync](../src/PlainOption/mapAsync.ts)
 - [option-t/PlainOption/mapOr](../src/PlainOption/mapOr.ts)
 - [option-t/PlainOption/mapOrElse](../src/PlainOption/mapOrElse.ts)
+- [option-t/PlainOption/mapOrElseAsync](../src/PlainOption/mapOrElseAsync.ts)
 - [option-t/PlainOption/okOr](../src/PlainOption/okOr.ts)
 - [option-t/PlainOption/okOrElse](../src/PlainOption/okOrElse.ts)
 - [option-t/PlainOption/Option](../src/PlainOption/Option.ts)
diff --git a/src/PlainOption/index.ts b/src/PlainOption/index.ts
index 633a4461..f2a569e5 100644
--- a/src/PlainOption/index.ts
+++ b/src/PlainOption/index.ts
@@ -37,6 +37,7 @@ export { mapForOption as map } from './map';
 export { mapAsyncForOption as mapAsync } from './mapAsync';
 export { mapOrForOption as mapOr } from './mapOr';
 export { mapOrElseForOption as mapOrElse } from './mapOrElse';
+export { mapOrElseAsyncForOption as mapOrElseAsync } from './mapOrElseAsync';
 export { orForOption as or } from './or';
 export { orElseForOption as orElse } from './orElse';
 export { orElseAsyncForOption as orElseAsync } from './orElseAsync';
diff --git a/src/PlainOption/mapOrElseAsync.ts b/src/PlainOption/mapOrElseAsync.ts
new file mode 100644
index 00000000..ab2ebce6
--- /dev/null
+++ b/src/PlainOption/mapOrElseAsync.ts
@@ -0,0 +1,36 @@
+import { assertIsPromise } from '../internal/assert';
+import {
+    ERR_MSG_RECOVERER_MUST_RETURN_PROMISE,
+    ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE,
+} from '../internal/ErrorMessage';
+import type { AsyncTransformFn, AsyncRecoveryFn } from '../internal/Function';
+import { Option, isNone } from './Option';
+import { unwrapOption } from './unwrap';
+
+/**
+ *  Maps a `Option<T>` to `U` by applying _transformer_ to a contained `Some(T)` value in _input_,
+ *  or a _recoverer_ function to a contained `None` value in _input_.
+ *  This function can be used to unpack a successful result while handling an error.
+ */
+export function mapOrElseAsyncForOption<T, U>(
+    input: Option<T>,
+    recoverer: AsyncRecoveryFn<U>,
+    transformer: AsyncTransformFn<T, U>
+): Promise<U> {
+    if (isNone(input)) {
+        const fallback: Promise<U> = 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;
+    }
+
+    const inner: T = unwrapOption(input);
+    const result: Promise<U> = transformer(inner);
+    // 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, ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE);
+    return result;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index c73e9140..65d7c545 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -139,6 +139,8 @@
     "cjs/PlainOption/mapOr.js": null,
     "cjs/PlainOption/mapOrElse.d.ts": null,
     "cjs/PlainOption/mapOrElse.js": null,
+    "cjs/PlainOption/mapOrElseAsync.d.ts": null,
+    "cjs/PlainOption/mapOrElseAsync.js": null,
     "cjs/PlainOption/okOr.d.ts": null,
     "cjs/PlainOption/okOr.js": null,
     "cjs/PlainOption/okOrElse.d.ts": null,
@@ -411,6 +413,8 @@
     "esm/PlainOption/mapOr.mjs": null,
     "esm/PlainOption/mapOrElse.d.ts": null,
     "esm/PlainOption/mapOrElse.mjs": null,
+    "esm/PlainOption/mapOrElseAsync.d.ts": null,
+    "esm/PlainOption/mapOrElseAsync.mjs": null,
     "esm/PlainOption/okOr.d.ts": null,
     "esm/PlainOption/okOr.mjs": null,
     "esm/PlainOption/okOrElse.d.ts": null,
@@ -747,6 +751,9 @@
     "lib/PlainOption/mapOrElse.d.ts": null,
     "lib/PlainOption/mapOrElse.js": null,
     "lib/PlainOption/mapOrElse.mjs": null,
+    "lib/PlainOption/mapOrElseAsync.d.ts": null,
+    "lib/PlainOption/mapOrElseAsync.js": null,
+    "lib/PlainOption/mapOrElseAsync.mjs": null,
     "lib/PlainOption/okOr.d.ts": null,
     "lib/PlainOption/okOr.js": null,
     "lib/PlainOption/okOr.mjs": null,
diff --git a/tools/public_api/table.mjs b/tools/public_api/table.mjs
index 54278e46..e1c013d8 100644
--- a/tools/public_api/table.mjs
+++ b/tools/public_api/table.mjs
@@ -272,6 +272,7 @@ export const apiTable = Object.freeze({
             'mapAsync',
             'mapOr',
             'mapOrElse',
+            'mapOrElseAsync',
             'or',
             'orElse',
             'orElseAsync',
@@ -349,6 +350,11 @@ export const apiTable = Object.freeze({
             'mapOrElseForOption'
         ]
     },
+    'PlainOption/mapOrElseAsync': {
+        'exports': [
+            'mapOrElseAsyncForOption'
+        ]
+    },
     'PlainOption/okOr': {
         'exports': [
             'okOrForPlainOption'
@@ -813,6 +819,7 @@ export const legacyApiTable = Object.freeze({
     'cjs/PlainOption/mapAsync': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/mapOr': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/mapOrElse': COMPAT_DESCRIPTOR,
+    'cjs/PlainOption/mapOrElseAsync': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/okOr': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/or': COMPAT_DESCRIPTOR,
@@ -944,6 +951,7 @@ export const legacyApiTable = Object.freeze({
     'esm/PlainOption/mapAsync': COMPAT_DESCRIPTOR,
     'esm/PlainOption/mapOr': COMPAT_DESCRIPTOR,
     'esm/PlainOption/mapOrElse': COMPAT_DESCRIPTOR,
+    'esm/PlainOption/mapOrElseAsync': COMPAT_DESCRIPTOR,
     'esm/PlainOption/okOr': COMPAT_DESCRIPTOR,
     'esm/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'esm/PlainOption/or': COMPAT_DESCRIPTOR,
@@ -1075,6 +1083,7 @@ export const legacyApiTable = Object.freeze({
     'lib/PlainOption/mapAsync': COMPAT_DESCRIPTOR,
     'lib/PlainOption/mapOr': COMPAT_DESCRIPTOR,
     'lib/PlainOption/mapOrElse': COMPAT_DESCRIPTOR,
+    'lib/PlainOption/mapOrElseAsync': COMPAT_DESCRIPTOR,
     'lib/PlainOption/okOr': COMPAT_DESCRIPTOR,
     'lib/PlainOption/okOrElse': COMPAT_DESCRIPTOR,
     'lib/PlainOption/or': COMPAT_DESCRIPTOR,


Test patch:
diff --git a/__tests__/PlainOption/map_or_else_async.test.mjs b/__tests__/PlainOption/map_or_else_async.test.mjs
new file mode 100644
index 00000000..98c9de4e
--- /dev/null
+++ b/__tests__/PlainOption/map_or_else_async.test.mjs
@@ -0,0 +1,103 @@
+import test from 'ava';
+
+import { createSome, createNone } from '../../__dist/esm/PlainOption/Option.mjs';
+import { mapOrElseAsyncForOption } from '../../__dist/esm/PlainOption/mapOrElseAsync.mjs';
+
+const PLAN_COUNT = 3;
+
+test('Some<T>', async (t) => {
+    const INITIAL = 1;
+    const EXPECTED = 3;
+
+    t.plan(PLAN_COUNT);
+
+    const input = createSome(INITIAL);
+    const result = mapOrElseAsyncForOption(
+        input,
+        async (_e) => {
+            t.fail(`don't enter this path`);
+        },
+        async (v) => {
+            t.is(v, INITIAL, 'the argument');
+            return EXPECTED;
+        }
+    );
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, 3, 'the return value');
+});
+
+test('None', async (t) => {
+    const EXPECTED = Math.random();
+
+    t.plan(PLAN_COUNT);
+
+    const input = createNone();
+    const result = mapOrElseAsyncForOption(
+        input,
+        async () => {
+            t.pass(true);
+            return EXPECTED;
+        },
+        async (_v) => {
+            t.fail(`don't enter this path`);
+        }
+    );
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, EXPECTED, 'the return value');
+});
+
+test('input is Some, callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const input = createSome(Math.random);
+            await mapOrElseAsyncForOption(
+                input,
+                (_e) => {
+                    t.fail(`don't enter this path`);
+                    return Math.random();
+                },
+                (_v) => {
+                    t.pass();
+                    return Math.random();
+                }
+            );
+        },
+        {
+            instanceOf: TypeError,
+            message: '`transformer` must return Promise',
+        }
+    );
+});
+
+test('input is None, callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const input = createNone();
+            await mapOrElseAsyncForOption(
+                input,
+                (_e) => {
+                    t.pass();
+                    return Math.random();
+                },
+                (_v) => {
+                    t.fail(`don't enter this path`);
+                    return Math.random();
+                }
+            );
+        },
+        {
+            instanceOf: TypeError,
+            message: '`recoverer` must return Promise',
+        }
+    );
+});