Repository: char0n/ramda-adjunct (js)

Issue:
async
Creates async interface from generator. https://www.promisejs.org/generators/

```js
const responsesP = async(function *() {
  const response1 = yield request('http://localhost:8000');
  const response2 = yield request('http://localhost:8000/2');

  return { response1, response2 };
});

responsesP().then(console.dir);
```
Yield delegation and recursion delegatin must be tested too to guarantee correctness of the implementation.


Pull request description:
feat(async): add initial implementation
Closes #830



Patch:
diff --git a/src/async.js b/src/async.js
new file mode 100644
index 0000000000..98c0a1dd73
--- /dev/null
+++ b/src/async.js
@@ -0,0 +1,61 @@
+import { curryN, bind } from 'ramda';
+import curry1 from 'ramda/src/internal/_curry1';
+
+import resolveP from './resolveP';
+import rejectP from './rejectP';
+
+/**
+ * Takes a generator function and returns an async function.
+ * The async function returned is a curried function whose arity matches that of the generator function.
+ *
+ * Note: This function is handy for environments that does support generators but doesn't support async/await.
+ *
+ * @func async
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/2.16.0|v2.16.0}
+ * @category Function
+ * @sig Promise c => (a, b, ...) -> a -> b -> ... -> c
+ * @param {Function} generatorFn The generator function
+ * @return {Function} Curried async function
+ * @see {@link https://www.promisejs.org/generators/}
+ * @example
+ *
+ * const asyncFn = RA.async(function* generator(val1, val2) {
+ *   const a = yield Promise.resolve(val1);
+ *   const b = yield Promise.resolve(val2);
+ *
+ *   return a + b;
+ * });
+ *
+ * asyncFn(1, 2); //=> Promise(3)
+ *
+ */
+const async = curry1(generatorFn => {
+  function asyncWrapper(...args) {
+    const iterator = bind(generatorFn, this)(...args);
+
+    const handle = result => {
+      const resolved = resolveP(result.value);
+
+      return result.done
+        ? resolved
+        : resolved.then(
+            value => handle(iterator.next(value)),
+            error => handle(iterator.throw(error))
+          );
+    };
+
+    try {
+      return handle(iterator.next());
+    } catch (error) {
+      return rejectP(error);
+    }
+  }
+
+  if (generatorFn.length > 0) {
+    return curryN(generatorFn.length, asyncWrapper);
+  }
+  return asyncWrapper;
+});
+
+export default async;
diff --git a/src/index.d.ts b/src/index.d.ts
index 33aad9e64f..37c26f6d19 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,3 +1,5 @@
+import { GeneratorFn } from 'jsverify';
+
 declare var RA: RamdaAdjunct.Static;
 
 declare namespace RamdaAdjunct {
@@ -1085,6 +1087,14 @@ declare namespace RamdaAdjunct {
          */
         sign(val: number): number;
 
+        /**
+         * Takes a generator function and returns an async function.
+         * The async function returned is a curried function whose arity matches that of the generator function.
+         *
+         * Note: This function is handy for environments that does support generators but doesn't support async/await.
+         */
+        async(generatorFn: Function): Function;
+
          /**
           * Identity type.
           */
diff --git a/src/index.js b/src/index.js
index 0fb91926f7..c3beb924d0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -96,6 +96,7 @@ export { default as Y } from './Y';
 export { default as seq } from './seq';
 export { default as sequencing } from './seq';
 export { default as dispatch } from './dispatch';
+export { default as async } from './async';
 // List
 export { default as mapIndexed } from './mapIndexed';
 export { default as reduceIndexed } from './reduceIndexed';


Test patch:
diff --git a/test/async.js b/test/async.js
new file mode 100644
index 0000000000..72f830d8dd
--- /dev/null
+++ b/test/async.js
@@ -0,0 +1,146 @@
+import * as R from 'ramda';
+import { assert } from 'chai';
+
+import eq from './shared/eq';
+import * as RA from '../src';
+
+describe('async', function() {
+  context('given wrapping of generator', function() {
+    specify('should mimic async/await behavior', async function() {
+      const asyncFn = RA.async(function* generator(val1, val2) {
+        const a = yield RA.resolveP(val1);
+        const b = yield RA.resolveP(val2);
+
+        return a + b;
+      });
+      const expected = await asyncFn(1, 2);
+
+      eq(expected, 3);
+    });
+
+    context('and the generator throw Error', function() {
+      specify('should resolve with rejection', async function() {
+        const asyncFn = RA.async(function* generator(val1, val2) {
+          yield RA.resolveP(val1);
+          yield RA.resolveP(val2);
+
+          throw new Error('generator error');
+        });
+
+        try {
+          await asyncFn(1, 2);
+          throw new Error('fulfilling should fail');
+        } catch (error) {
+          assert.instanceOf(error, Error);
+          eq(error.message, 'generator error');
+        }
+      });
+    });
+  });
+
+  it('should support yield delegation', async function() {
+    const foo = function* generator(val1, val2) {
+      const a = yield RA.resolveP(val1);
+      const b = yield RA.resolveP(val2);
+
+      return a + b;
+    };
+    const bar = RA.async(function* generator(val1, val2) {
+      const a = yield RA.resolveP(val1);
+      const b = yield RA.resolveP(val2);
+      const c = yield* foo(a, b);
+
+      return c + 3;
+    });
+
+    eq(await bar(1, 2), 6);
+  });
+
+  it('should support async delegation', async function() {
+    const foo = RA.async(function* generator(val1, val2) {
+      const a = yield RA.resolveP(val1);
+      const b = yield RA.resolveP(val2);
+
+      return a + b;
+    });
+    const bar = RA.async(function* generator(val1, val2) {
+      const a = yield RA.resolveP(val1);
+      const b = yield RA.resolveP(val2);
+      const c = yield foo(a, b);
+
+      return c + 3;
+    });
+
+    eq(await bar(1, 2), 6);
+  });
+
+  it('should support recursion delegation', async function() {
+    const async = RA.async(function* generator(val) {
+      let newVal = val;
+
+      if (val > 1) {
+        newVal = yield async(val - 1);
+      }
+
+      return yield newVal;
+    });
+
+    eq(await async(10), 1);
+  });
+
+  it('should curry', async function() {
+    const async = RA.async(R.__);
+    const asyncFn = async(function* generator() {
+      yield RA.resolveP(1);
+      return 2;
+    });
+    const expected = await asyncFn();
+
+    eq(expected, 2);
+  });
+
+  context('given wrapping of generator with arity of 2', function() {
+    let asyncFn;
+
+    beforeEach(function() {
+      // eslint-disable-next-line require-yield
+      asyncFn = RA.async(function* generator(a, b) {
+        return a + b;
+      });
+    });
+
+    specify('should translate generator arity to wrapper', function() {
+      eq(asyncFn.length, 2);
+    });
+
+    specify('should curry wrapper to appropriate arity', async function() {
+      eq(await asyncFn(1, 2), 3);
+      eq(await asyncFn(1)(2), 3);
+    });
+  });
+
+  context('given wrapping of generator with arity of 0', function() {
+    context('then the resulting wrapper', function() {
+      let asyncFn;
+
+      beforeEach(function() {
+        // eslint-disable-next-line require-yield
+        asyncFn = RA.async(function* generator() {
+          return 1;
+        });
+      });
+
+      specify('should not support placeholder', async function() {
+        const expected = await asyncFn(R.__);
+
+        eq(expected, 1);
+      });
+
+      specify('should support call without arguments', async function() {
+        const expected = await asyncFn();
+
+        eq(expected, 1);
+      });
+    });
+  });
+});