Repository: karen-irc/option-t
Pull request #1037 (2021-06-06 15:35:54)

Implement `unwrapOrElseAsync()` for PlainOption
Fix  #930


Patch:
diff --git a/docs/public_api_list.md b/docs/public_api_list.md
index 5a233eb5..414b1977 100644
--- a/docs/public_api_list.md
+++ b/docs/public_api_list.md
@@ -86,6 +86,7 @@
 - [option-t/PlainOption/unwrap](../src/PlainOption/unwrap.ts)
 - [option-t/PlainOption/unwrapOr](../src/PlainOption/unwrapOr.ts)
 - [option-t/PlainOption/unwrapOrElse](../src/PlainOption/unwrapOrElse.ts)
+- [option-t/PlainOption/unwrapOrElseAsync](../src/PlainOption/unwrapOrElseAsync.ts)
 - [option-t/PlainOption/xor](../src/PlainOption/xor.ts)
 
 
diff --git a/src/PlainOption/index.ts b/src/PlainOption/index.ts
index 246617a6..633a4461 100644
--- a/src/PlainOption/index.ts
+++ b/src/PlainOption/index.ts
@@ -45,3 +45,4 @@ export { tapOption as tap } from './tap';
 export { unwrapOption as unwrap } from './unwrap';
 export { unwrapOrFromOption as unwrapOr } from './unwrapOr';
 export { unwrapOrElseFromOption as unwrapOrElse } from './unwrapOrElse';
+export { unwrapOrElseAsyncFromOption as unwrapOrElseAsync } from './unwrapOrElseAsync';
diff --git a/src/PlainOption/unwrapOrElseAsync.ts b/src/PlainOption/unwrapOrElseAsync.ts
new file mode 100644
index 00000000..7b33ee95
--- /dev/null
+++ b/src/PlainOption/unwrapOrElseAsync.ts
@@ -0,0 +1,26 @@
+import { assertIsPromise } from '../internal/assert';
+import { ERR_MSG_RECOVERER_MUST_RETURN_PROMISE } from '../internal/ErrorMessage';
+import type { AsyncRecoveryFn } from '../internal/Function';
+import { Option, isSome } from './Option';
+import { unwrapOption } from './unwrap';
+
+/**
+ *  Unwraps a result _input_, returns the content of an `Some(T)`.
+ *  If the value is an `None` then it calls `def` with its value.
+ */
+export function unwrapOrElseAsyncFromOption<T>(
+    input: Option<T>,
+    recoverer: AsyncRecoveryFn<T>
+): Promise<T> {
+    if (isSome(input)) {
+        const val: T = unwrapOption(input);
+        return Promise.resolve(val);
+    }
+
+    const defaultValue: Promise<T> = 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(defaultValue, ERR_MSG_RECOVERER_MUST_RETURN_PROMISE);
+    return defaultValue;
+}
diff --git a/tools/pkg_files.json b/tools/pkg_files.json
index 627227ad..c73e9140 100644
--- a/tools/pkg_files.json
+++ b/tools/pkg_files.json
@@ -159,6 +159,8 @@
     "cjs/PlainOption/unwrapOr.js": null,
     "cjs/PlainOption/unwrapOrElse.d.ts": null,
     "cjs/PlainOption/unwrapOrElse.js": null,
+    "cjs/PlainOption/unwrapOrElseAsync.d.ts": null,
+    "cjs/PlainOption/unwrapOrElseAsync.js": null,
     "cjs/PlainOption/xor.d.ts": null,
     "cjs/PlainOption/xor.js": null,
     "cjs/PlainResult/Result.d.ts": null,
@@ -429,6 +431,8 @@
     "esm/PlainOption/unwrapOr.mjs": null,
     "esm/PlainOption/unwrapOrElse.d.ts": null,
     "esm/PlainOption/unwrapOrElse.mjs": null,
+    "esm/PlainOption/unwrapOrElseAsync.d.ts": null,
+    "esm/PlainOption/unwrapOrElseAsync.mjs": null,
     "esm/PlainOption/xor.d.ts": null,
     "esm/PlainOption/xor.mjs": null,
     "esm/PlainResult/Result.d.ts": null,
@@ -773,6 +777,9 @@
     "lib/PlainOption/unwrapOrElse.d.ts": null,
     "lib/PlainOption/unwrapOrElse.js": null,
     "lib/PlainOption/unwrapOrElse.mjs": null,
+    "lib/PlainOption/unwrapOrElseAsync.d.ts": null,
+    "lib/PlainOption/unwrapOrElseAsync.js": null,
+    "lib/PlainOption/unwrapOrElseAsync.mjs": null,
     "lib/PlainOption/xor.d.ts": null,
     "lib/PlainOption/xor.js": null,
     "lib/PlainOption/xor.mjs": null,
diff --git a/tools/public_api/table.mjs b/tools/public_api/table.mjs
index eabf15fc..54278e46 100644
--- a/tools/public_api/table.mjs
+++ b/tools/public_api/table.mjs
@@ -279,7 +279,8 @@ export const apiTable = Object.freeze({
             'tap',
             'unwrap',
             'unwrapOr',
-            'unwrapOrElse'
+            'unwrapOrElse',
+            'unwrapOrElseAsync',
         ]
     },
 
@@ -406,6 +407,11 @@ export const apiTable = Object.freeze({
             'unwrapOrElseFromOption'
         ]
     },
+    'PlainOption/unwrapOrElseAsync': {
+        'exports': [
+            'unwrapOrElseAsyncFromOption'
+        ]
+    },
     'PlainOption/xor': {
         'exports': [
             'xorForOption'
@@ -817,6 +823,7 @@ export const legacyApiTable = Object.freeze({
     'cjs/PlainOption/unwrap': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/unwrapOr': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/unwrapOrElse': COMPAT_DESCRIPTOR,
+    'cjs/PlainOption/unwrapOrElseAsync': COMPAT_DESCRIPTOR,
     'cjs/PlainOption/xor': COMPAT_DESCRIPTOR,
     'cjs/PlainResult/Result': COMPAT_DESCRIPTOR,
     'cjs/PlainResult/and': COMPAT_DESCRIPTOR,
@@ -947,6 +954,7 @@ export const legacyApiTable = Object.freeze({
     'esm/PlainOption/unwrap': COMPAT_DESCRIPTOR,
     'esm/PlainOption/unwrapOr': COMPAT_DESCRIPTOR,
     'esm/PlainOption/unwrapOrElse': COMPAT_DESCRIPTOR,
+    'esm/PlainOption/unwrapOrElseAsync': COMPAT_DESCRIPTOR,
     'esm/PlainOption/xor': COMPAT_DESCRIPTOR,
     'esm/PlainResult/Result': COMPAT_DESCRIPTOR,
     'esm/PlainResult/and': COMPAT_DESCRIPTOR,
@@ -1077,6 +1085,7 @@ export const legacyApiTable = Object.freeze({
     'lib/PlainOption/unwrap': COMPAT_DESCRIPTOR,
     'lib/PlainOption/unwrapOr': COMPAT_DESCRIPTOR,
     'lib/PlainOption/unwrapOrElse': COMPAT_DESCRIPTOR,
+    'lib/PlainOption/unwrapOrElseAsync': COMPAT_DESCRIPTOR,
     'lib/PlainOption/xor': COMPAT_DESCRIPTOR,
     'lib/PlainResult/Result': COMPAT_DESCRIPTOR,
     'lib/PlainResult/and': COMPAT_DESCRIPTOR,


Test patch:
diff --git a/__tests__/PlainOption/unwrap_or_else_async.test.mjs b/__tests__/PlainOption/unwrap_or_else_async.test.mjs
new file mode 100644
index 00000000..080d5fa2
--- /dev/null
+++ b/__tests__/PlainOption/unwrap_or_else_async.test.mjs
@@ -0,0 +1,55 @@
+import test from 'ava';
+
+import { createSome, createNone } from '../../__dist/esm/PlainOption/Option.mjs';
+import { unwrapOrElseAsyncFromOption } from '../../__dist/esm/PlainOption/unwrapOrElseAsync.mjs';
+
+const VALUE_T = Math.random();
+const DEFAULT_VAL = Math.random();
+
+test('input is Some(T)', async (t) => {
+    t.plan(2);
+
+    const input = createSome(VALUE_T);
+    const result = unwrapOrElseAsyncFromOption(input, async () => {
+        t.pass(false);
+        return DEFAULT_VAL;
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, VALUE_T);
+});
+
+test('input is None', async (t) => {
+    t.plan(3);
+
+    const input = createNone();
+    const result = unwrapOrElseAsyncFromOption(input, async () => {
+        t.pass(true);
+        return DEFAULT_VAL;
+    });
+
+    t.true(result instanceof Promise, 'result should be Promise');
+
+    const actual = await result;
+    t.is(actual, DEFAULT_VAL);
+});
+
+test('callback should return Promise', async (t) => {
+    t.plan(2);
+
+    await t.throwsAsync(
+        async () => {
+            const input = createNone();
+            await unwrapOrElseAsyncFromOption(input, () => {
+                t.pass();
+                return createNone();
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: '`recoverer` must return Promise',
+        }
+    );
+});