Repository: ViacomInc/data-point (js)

Issue:
asArray reducer helper
**Problem description**:

When defining reducers, arrays become `ReducerLists`, but that might not always be the desired behavior.

In this code, the value of `transform:example` is equal to the resolved value of `request:2`:

```js
{
  'transform:example': [
    'request:1',
    'request:2'
  ]
}
```

However, if you want the result to be an array where `array[0]` is the result of `request:1` and `array[1]` is the result of `request:2`, the code gets clunky:

```js
{
  'transform:example': [
    {
      a: 'request:1',
      b: 'request:2'
    },
    [
      '$a',
      '$b'
    ]
  ]
}
```


**Suggested solution**:

Create an `asArray` reducer helper. This helper would take an array of reducers, resolve each of them, and return an array of the resolved values.

```js
const { asArray } = require('data-point').helpers

{
  // the result will be an array of length 2 
  'transform:example': asArray([
    'request:1',
    'request:2'
  ])
}

Pull request:
feat(data-point/reducer-parallel): Add ReducerParallel type
**What**: Adds the `parallel` reducer (closes #168, where it was called `asArray` instead of `parallel`)

<!-- Why are these changes necessary? -->
**Why**: This makes it easy to resolve an array of reducers and keep the output from each of them (instead of piping the result of each reducer into the next one like `ReducerList`).

<!-- How were these changes implemented? -->
**How**: Adds a new reducer type.

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Has Breaking changes
- [x] Documentation
- [x] Tests
- [x] Ready to be merged

Patch:
diff --git a/packages/data-point/README.md b/packages/data-point/README.md
index e028106..19833be 100644
--- a/packages/data-point/README.md
+++ b/packages/data-point/README.md
@@ -37,6 +37,7 @@ npm install --save data-point
   - [filter](#reducer-filter)
   - [find](#reducer-find)
   - [constant](#reducer-constant)
+  - [parallel](#reducer-parallel)
 - [Entities](#entities)
   - [dataPoint.addEntities](#api-data-point-add-entities)
   - [Built-in entities](#built-in-entities)
@@ -1188,6 +1189,45 @@ constant(value:*):*
   ```
 </details>
 
+### <a name="reducer-parallel">parallel</a>
+
+This resolves an array of reducers. The output is a new array where each element is the output of a reducer;
+this contrasts with `ListReducer`, which returns the output from the last reducer in the array.
+
+**SYNOPSIS**
+
+```js
+parallel(reducers:Array<Reducer>):Array
+```
+
+**Reducer's arguments**
+
+| Argument | Type | Description |
+|:---|:---|:---|
+| *reducers* | Array<Reducer> | Source data to create an array of [reducers](#reducers) |
+
+**EXAMPLE:**
+
+<details>
+  <summary>resolving an array of reducers with parallel</summary>
+
+  ```js
+  const { parallel } = DataPoint.helpers
+
+  const reducer = parallel([
+    '$a',
+    ['$b', (input) => input + 2] // ReducerList
+  ])
+
+  const input = {
+    a: 1,
+    b: 2
+  }
+
+  dataPoint.resolve(reducer, input) // => [1, 4]
+  ```
+</details>
+
 ## <a name="entities">Entities</a>
 
 Entities are artifacts that transform data. An entity is represented by a data structure (spec) that defines how the entity behaves. 
diff --git a/packages/data-point/lib/helpers/helpers.js b/packages/data-point/lib/helpers/helpers.js
index 2fe8424..4ae35d0 100644
--- a/packages/data-point/lib/helpers/helpers.js
+++ b/packages/data-point/lib/helpers/helpers.js
@@ -13,6 +13,7 @@ module.exports.helpers = {
   find: stubFactories.find,
   map: stubFactories.map,
   omit: stubFactories.omit,
+  parallel: stubFactories.parallel,
   pick: stubFactories.pick,
   isString: typeCheckFunctionReducers.isString,
   isNumber: typeCheckFunctionReducers.isNumber,
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/index.js b/packages/data-point/lib/reducer-types/reducer-helpers/index.js
index 0bce23c..64587e9 100644
--- a/packages/data-point/lib/reducer-types/reducer-helpers/index.js
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/index.js
@@ -6,6 +6,7 @@ const reducerFilter = require('./reducer-filter')
 const reducerFind = require('./reducer-find')
 const reducerMap = require('./reducer-map')
 const reducerOmit = require('./reducer-omit')
+const reducerParallel = require('./reducer-parallel')
 const reducerPick = require('./reducer-pick')
 
 /*
@@ -36,6 +37,7 @@ const reducers = {
   [reducerFind.type]: reducerFind,
   [reducerMap.type]: reducerMap,
   [reducerOmit.type]: reducerOmit,
+  [reducerParallel.type]: reducerParallel,
   [reducerPick.type]: reducerPick
 }
 
@@ -52,6 +54,7 @@ const stubFactories = {
   [reducerFind.name]: bindStubFunction(reducerFind.type),
   [reducerMap.name]: bindStubFunction(reducerMap.type),
   [reducerOmit.name]: bindStubFunction(reducerOmit.type),
+  [reducerParallel.name]: bindStubFunction(reducerParallel.type),
   [reducerPick.name]: bindStubFunction(reducerPick.type)
 }
 
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.js
new file mode 100644
index 0000000..ee4c946
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.js
@@ -0,0 +1,35 @@
+const REDUCER_PARALLEL = 'ReducerParallel'
+
+module.exports.type = REDUCER_PARALLEL
+
+const HELPER_NAME = 'parallel'
+
+module.exports.name = HELPER_NAME
+
+/**
+ * @class
+ * @property {string} type
+ * @property {Array<reducer>} reducers
+ */
+function ReducerParallel () {
+  this.type = 'ReducerParallel'
+  this.reducers = []
+}
+
+module.exports.ReducerParallel = ReducerParallel
+
+/**
+ * @param {Function} createReducer
+ * @param {Array} source
+ * @return {reducer}
+ */
+function create (createReducer, source) {
+  const reducers = source.map(token => createReducer(token))
+
+  const reducer = new ReducerParallel()
+  reducer.reducers = reducers
+
+  return reducer
+}
+
+module.exports.create = create
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/index.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/index.js
new file mode 100644
index 0000000..fafee9c
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/index.js
@@ -0,0 +1,9 @@
+const factory = require('./factory')
+const resolve = require('./resolve').resolve
+
+module.exports = {
+  create: factory.create,
+  type: factory.type,
+  name: factory.name,
+  resolve: resolve
+}
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.js
new file mode 100644
index 0000000..589b904
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.js
@@ -0,0 +1,21 @@
+const Promise = require('bluebird')
+
+const utils = require('../../../utils')
+
+/**
+ * @param {Object} manager
+ * @param {Function} resolveReducer
+ * @param {Accumulator} accumulator
+ * @param {ReducerParallel} reducerParallel
+ * @returns {Promise<Accumulator>}
+ */
+function resolve (manager, resolveReducer, accumulator, reducerParallel) {
+  return Promise.map(reducerParallel.reducers, reducer => {
+    return resolveReducer(manager, accumulator, reducer)
+  }).then(result => {
+    const value = result.map(acc => acc.value)
+    return utils.assign(accumulator, { value })
+  })
+}
+
+module.exports.resolve = resolve


Test patch:
diff --git a/packages/data-point/lib/__snapshots__/index.test.js.snap b/packages/data-point/lib/__snapshots__/index.test.js.snap
index 572af7e..bc08161 100644
--- a/packages/data-point/lib/__snapshots__/index.test.js.snap
+++ b/packages/data-point/lib/__snapshots__/index.test.js.snap
@@ -19,6 +19,7 @@ Object {
     "isString": [Function],
     "map": [Function],
     "omit": [Function],
+    "parallel": [Function],
     "pick": [Function],
   },
   "reducify": [Function],
diff --git a/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap b/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap
index 2ec9bee..22a99f0 100644
--- a/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap
+++ b/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap
@@ -20,6 +20,7 @@ Object {
     "isString": [Function],
     "map": [Function],
     "omit": [Function],
+    "parallel": [Function],
     "pick": [Function],
   },
   "isReducer": [Function],
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/__snapshots__/factory.test.js.snap b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/__snapshots__/factory.test.js.snap
new file mode 100644
index 0000000..d7fcb6d
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/__snapshots__/factory.test.js.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReducerParallel#factory It should create a ReducerParallel object 1`] = `
+Array [
+  ReducerPath {
+    "asCollection": false,
+    "body": [Function],
+    "name": "a",
+    "type": "ReducerPath",
+    Symbol(reducer): true,
+  },
+  ReducerFunction {
+    "body": [Function],
+    "type": "ReducerFunction",
+    Symbol(reducer): true,
+  },
+]
+`;
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.test.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.test.js
new file mode 100644
index 0000000..cbc7105
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/factory.test.js
@@ -0,0 +1,14 @@
+/* eslint-env jest */
+
+const Factory = require('./factory')
+const createReducer = require('../../index').create
+
+describe('ReducerParallel#factory', () => {
+  test('It should create a ReducerParallel object', () => {
+    const source = ['$a', () => true]
+    const reducer = Factory.create(createReducer, source)
+    expect(reducer).toBeInstanceOf(Factory.ReducerParallel)
+    expect(reducer.type).toBe(Factory.type)
+    expect(reducer.reducers).toMatchSnapshot()
+  })
+})
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.test.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.test.js
new file mode 100644
index 0000000..252e64b
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-parallel/resolve.test.js
@@ -0,0 +1,57 @@
+/* eslint-env jest */
+
+const Factory = require('./factory')
+const Resolve = require('./resolve')
+
+const Reducer = require('../../index')
+
+const DataPoint = require('../../../index')
+
+const AccumulatorFactory = require('../../../accumulator/factory')
+
+let manager
+
+beforeAll(() => {
+  manager = DataPoint.create()
+})
+
+describe('ReducerOmit#resolve', () => {
+  test('return empty array when no reducers are provided', () => {
+    const value = []
+    const accumulator = AccumulatorFactory.create({ value })
+    const reducer = Factory.create(Reducer.create, [])
+    return Resolve.resolve(manager, Reducer.resolve, accumulator, reducer).then(
+      result => {
+        expect(result.value).toEqual(value)
+      }
+    )
+  })
+  test('returns array of length 3 when 3 reducers are provided', () => {
+    const value = {
+      a: 1,
+      b: 2,
+      c: 3
+    }
+    const accumulator = AccumulatorFactory.create({ value })
+    const reducer = Factory.create(Reducer.create, [
+      '$a',
+      {
+        b: '$b'
+      },
+      ['$c', input => input + 2],
+      DataPoint.helpers.parallel(['$a', '$b', '$c'])
+    ])
+    return Resolve.resolve(manager, Reducer.resolve, accumulator, reducer).then(
+      result => {
+        expect(result.value).toEqual([
+          1,
+          {
+            b: 2
+          },
+          5,
+          [1, 2, 3]
+        ])
+      }
+    )
+  })
+})