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

Issue:
Implement mapAsync for PlainOption

Pull request description:
Implement mapAsync for PlainOption
Fix #925

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index ee38053b..ace0af05 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -71,6 +71,7 @@
 - [option-t/PlainOption/filter](../src/PlainOption/filter.ts)
 - [option-t/PlainOption/flatten](../src/PlainOption/flatten.ts)
 - [option-t/PlainOption/map](../src/PlainOption/map.ts)
+- [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/okOr](../src/PlainOption/okOr.ts)
diff --git a/src/PlainOption/index.ts b/src/PlainOption/index.ts
index ddfff433..6570752c 100644
--- a/src/PlainOption/index.ts
+++ b/src/PlainOption/index.ts
@@ -33,6 +33,7 @@ export { expectIsSome as expect } from './expect';
 export { filterForOption as filter } from './filter';
 export { flattenForOption as flatten } from './flatten';
 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 { orForOption as or } from './or';
diff --git a/src/PlainOption/mapAsync.ts b/src/PlainOption/mapAsync.ts
new file mode 100644
index 00000000..6dfc6379
--- /dev/null
+++ b/src/PlainOption/mapAsync.ts
@@ -0,0 +1,32 @@
+import { assertIsPromise } from '../internal/assert';
+import { ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE } from '../internal/ErrorMessage';
+import type { AsyncTransformFn } from '../internal/Function';
+
+import { Option, createSome, isNone } from './Option';
+import { unwrapOption } from './unwrap';
+
+/**
+ *  Maps a `Option<T>` to `Option<U>` by applying a _transformer_ function
+ *  to an contained `Some(T)` value, leaving an `None` value untouched.
+ *
+ *  This function can be used to compose the results of two functions.
+ */
+export function mapAsyncForOption<T, U>(
+    input: Option<T>,
+    transformer: AsyncTransformFn<T, U>
+): Promise<Option<U>> {
+    if (isNone(input)) {
+        return Promise.resolve(input);
+    }
+
+    const inner = 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);
+
+    const wrapped: Promise<Option<U>> = result.then(createSome);
+    return wrapped;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 75dc491f..bea07b30 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -163,6 +163,8 @@
     "cjs/PlainOption/index.js": null,
     "cjs/PlainOption/map.d.ts": null,
     "cjs/PlainOption/map.js": null,
+    "cjs/PlainOption/mapAsync.d.ts": null,
+    "cjs/PlainOption/mapAsync.js": null,
     "cjs/PlainOption/mapOr.d.ts": null,
     "cjs/PlainOption/mapOr.js": null,
     "cjs/PlainOption/mapOrElse.d.ts": null,
@@ -459,6 +461,8 @@
     "esm/PlainOption/index.mjs": null,
     "esm/PlainOption/map.d.ts": null,
     "esm/PlainOption/map.mjs": null,
+    "esm/PlainOption/mapAsync.d.ts": null,
+    "esm/PlainOption/mapAsync.mjs": null,
     "esm/PlainOption/mapOr.d.ts": null,
     "esm/PlainOption/mapOr.mjs": null,
     "esm/PlainOption/mapOrElse.d.ts": null,
@@ -831,6 +835,9 @@
     "lib/PlainOption/map.d.ts": null,
     "lib/PlainOption/map.js": null,
     "lib/PlainOption/map.mjs": null,
+    "lib/PlainOption/mapAsync.d.ts": null,
+    "lib/PlainOption/mapAsync.js": null,
+    "lib/PlainOption/mapAsync.mjs": null,
     "lib/PlainOption/mapOr.d.ts": null,
     "lib/PlainOption/mapOr.js": null,
     "lib/PlainOption/mapOr.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index 50e952d1..2af2be82 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -268,6 +268,7 @@ export default {
             'filter',
             'flatten',
             'map',
+            'mapAsync',
             'mapOr',
             'mapOrElse',
             'or',
@@ -325,6 +326,11 @@ export default {
             'mapForOption'
         ]
     },
+    'PlainOption/mapAsync': {
+        'exports': [
+            'mapAsyncForOption'
+        ]
+    },
     'PlainOption/mapOr': {
         'exports': [
             'mapOrForOption'


Test patch:
diff --git a/__tests__/PlainOption/map_async.test.mjs b/__tests__/PlainOption/map_async.test.mjs
new file mode 100644
index 00000000..b3d931e9
--- /dev/null
+++ b/__tests__/PlainOption/map_async.test.mjs
@@ -0,0 +1,58 @@
+import test from 'ava';
+
+import { createSome, createNone } from '../../__dist/esm/PlainOption/Option.mjs';
+import { mapAsyncForOption } from '../../__dist/esm/PlainOption/mapAsync.mjs';
+
+const VALUE_T = Math.random();
+const VALUE_U = Math.random();
+
+test('input is Some(T)', async (t) => {
+    t.plan(5);
+
+    const input = createSome(VALUE_T);
+    const result = mapAsyncForOption(input, async (v) => {
+        t.pass(v, VALUE_T);
+        return VALUE_U;
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+
+    t.not(actual, input);
+    t.true(actual.ok);
+    t.is(actual.val, VALUE_U);
+});
+
+test('input is None', async (t) => {
+    t.plan(2);
+
+    const input = createNone();
+    const result = mapAsyncForOption(input, (v) => {
+        t.fail(v, VALUE_T);
+        return VALUE_U;
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(input, actual);
+});
+
+test('callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const input = createSome(VALUE_T);
+            await mapAsyncForOption(input, () => {
+                t.pass();
+                return 1;
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: '`transformer` must return Promise',
+        }
+    );
+});