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

Issue:
Implement andThenAsync for Maybe

Pull request:
Implement andThenAsync() for Maybe
Fix #931

Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 410f6c70..ed7e1e16 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -14,6 +14,7 @@
 - [option-t/Maybe](../src/Maybe/index.ts)
 - [option-t/Maybe/and](../src/Maybe/and.ts)
 - [option-t/Maybe/andThen](../src/Maybe/andThen.ts)
+- [option-t/Maybe/andThenAsync](../src/Maybe/andThenAsync.ts)
 - [option-t/Maybe/expect](../src/Maybe/expect.ts)
 - [option-t/Maybe/map](../src/Maybe/map.ts)
 - [option-t/Maybe/mapAsync](../src/Maybe/mapAsync.ts)
diff --git a/src/Maybe/andThenAsync.ts b/src/Maybe/andThenAsync.ts
new file mode 100644
index 00000000..7e87bb9c
--- /dev/null
+++ b/src/Maybe/andThenAsync.ts
@@ -0,0 +1,34 @@
+import { assertIsPromise } from '../internal/assert';
+import { ERR_MSG_TRANSFORMER_MUST_RETURN_PROMISE } from '../internal/ErrorMessage';
+import type { AsyncTransformFn } from '../internal/Function';
+
+import { Maybe, isNullOrUndefined } from './Maybe';
+
+export type MaybeAsyncTryTransformFn<T, U> = AsyncTransformFn<T, Maybe<U>>;
+
+/**
+ *  Returns `null` or `undefined` if the _input_ is `null` or `undefined`,
+ *  otherwise calls _transformer_ with the value and returns the result.
+ *
+ *  XXX:
+ *  Some languages call this operation flatmap.
+ *  But we don't provide `flatMap()` as alias of this function.
+ *  because it's too hard to undarstand that "flatMap" operation for `T | null | undefined`.
+ */
+export function andThenAsyncForMaybe<T, U>(
+    input: Maybe<T>,
+    transformer: MaybeAsyncTryTransformFn<T, U>
+): Promise<Maybe<U>> {
+    if (isNullOrUndefined(input)) {
+        return Promise.resolve(input);
+    }
+
+    const result: Promise<Maybe<U>> = transformer(input);
+
+    // 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/src/Maybe/index.ts b/src/Maybe/index.ts
index 5d3209a3..1947b20d 100644
--- a/src/Maybe/index.ts
+++ b/src/Maybe/index.ts
@@ -2,6 +2,7 @@ export { NotNullAndUndefined, Maybe, isNotNullAndUndefined, isNullOrUndefined }
 // XXX: `and()` operation is equivalent of `a && b` so we don't ship it by default set.
 //export { andForMaybe as and } from './and';
 export { andThenForMaybe as andThen } from './andThen';
+export { andThenAsyncForMaybe as andThenAsync } from './andThenAsync';
 export { expectNotNullAndUndefined as expect } from './expect';
 export { mapForMaybe as map } from './map';
 export { mapAsyncForMaybe as mapAsync } from './mapAsync';
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 1f7c81fc..8b5ac56b 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -59,6 +59,8 @@
     "cjs/Maybe/and.js": null,
     "cjs/Maybe/andThen.d.ts": null,
     "cjs/Maybe/andThen.js": null,
+    "cjs/Maybe/andThenAsync.d.ts": null,
+    "cjs/Maybe/andThenAsync.js": null,
     "cjs/Maybe/expect.d.ts": null,
     "cjs/Maybe/expect.js": null,
     "cjs/Maybe/index.d.ts": null,
@@ -345,6 +347,8 @@
     "esm/Maybe/and.mjs": null,
     "esm/Maybe/andThen.d.ts": null,
     "esm/Maybe/andThen.mjs": null,
+    "esm/Maybe/andThenAsync.d.ts": null,
+    "esm/Maybe/andThenAsync.mjs": null,
     "esm/Maybe/expect.d.ts": null,
     "esm/Maybe/expect.mjs": null,
     "esm/Maybe/index.d.ts": null,
@@ -655,6 +659,9 @@
     "lib/Maybe/andThen.d.ts": null,
     "lib/Maybe/andThen.js": null,
     "lib/Maybe/andThen.mjs": null,
+    "lib/Maybe/andThenAsync.d.ts": null,
+    "lib/Maybe/andThenAsync.js": null,
+    "lib/Maybe/andThenAsync.mjs": null,
     "lib/Maybe/expect.d.ts": null,
     "lib/Maybe/expect.js": null,
     "lib/Maybe/expect.mjs": null,
diff --git a/tools/public_api.mjs b/tools/public_api.mjs
index 21ce5dda..3c953b6e 100644
--- a/tools/public_api.mjs
+++ b/tools/public_api.mjs
@@ -10,6 +10,7 @@ export default {
             'isNotNullAndUndefined',
             'isNullOrUndefined',
             'andThen',
+            'andThenAsync',
             'expect',
             'map',
             'mapAsync',
@@ -32,6 +33,11 @@ export default {
             'andThenForMaybe'
         ]
     },
+    'Maybe/andThenAsync': {
+        'exports': [
+            'andThenAsyncForMaybe'
+        ]
+    },
     'Maybe/expect': {
         'exports': [
             'expectNotNullAndUndefined'


Test patch:
diff --git a/__tests__/Maybe/and_then_async.test.mjs b/__tests__/Maybe/and_then_async.test.mjs
new file mode 100644
index 00000000..8e0242a6
--- /dev/null
+++ b/__tests__/Maybe/and_then_async.test.mjs
@@ -0,0 +1,65 @@
+import test from 'ava';
+
+import { andThenAsyncForMaybe } from '../../__dist/esm/Maybe/andThenAsync.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, input, 'this is promise'];
+    }
+
+    return [input, input, input];
+});
+
+for (const [INPUT, PASSED_EXPECTED, FINAL_EXPECTED] of nonNullableValueCaseList) {
+    test('pass the value: ' + String(INPUT), async (t) => {
+        t.plan(4);
+
+        const result = andThenAsyncForMaybe(INPUT, async (v) => {
+            t.pass('should call selector fn');
+            t.is(v, PASSED_EXPECTED);
+            return v;
+        });
+
+        t.true(result instanceof Promise, 'result should be Promise');
+
+        const actual = await result;
+        t.is(actual, FINAL_EXPECTED);
+    });
+}
+
+for (const NULL_VALUE of [undefined, null]) {
+    test(`pass ${NULL_VALUE}`, async (t) => {
+        t.plan(2);
+
+        const result = andThenAsyncForMaybe(NULL_VALUE, async (_v) => {
+            t.fail('should not call selector fn');
+            return Math.random();
+        });
+
+        t.true(result instanceof Promise, 'result should be Promise');
+        const actual = await result;
+        t.is(actual, NULL_VALUE);
+    });
+
+    test(`input is ${NULL_VALUE}, callback should return Promise`, async (t) => {
+        t.plan(2);
+
+        await t.throwsAsync(
+            async () => {
+                const input = Math.random();
+                await andThenAsyncForMaybe(input, (_input) => {
+                    t.pass('should be called');
+                    return 1;
+                });
+            },
+            {
+                instanceOf: TypeError,
+                message: '`transformer` must return Promise',
+            }
+        );
+    });
+}