Repository: ViacomInc/data-point (js)

Issue:
PathReducer mapping
This would be a new feature, the idea is to use the same pattern for entity mapping which is to add `[]` at the end of an entity id:

`hash:Foo[]`

this feature would expect the same mapping behavior but using a ReducerPath: 

`$some.path[]`

By adding a `[]` at the end of a ReducerPath the developer should expect to get a mapping of the path passed. 

```js
const input = [
   { a: { b: { c: 'foo' }}},
   { a: { b: { c: 'bar' }}},
]
dataPoint.transform('$a.b.c[]', input)
  .then( acc => {
    // ['foo', 'bar']
    console.log(acc.value) 
  })
```

Pull request description:
Add path reducer mapping
This is an attempt to close #34.

Honestly I'm not sure if I accounted for all the places in the codebase to make this feature, but I've added the `asCollection` to the ReducerPath and resolve the ReducerPath if it encounters a path that ends in `[]`. I'm not sure if there's a way to use the value of `asCollection` to determine the method for resolving the path value, but if there is I can make that change.

~~Also, I didn't include it in this PR, but a basic installation of this library with node 8.7.0 and npm 5.4.2 introduces a package-lock.json, and `yarn install` updates the yarn.lock. Furthermore the `master` branch throws errors on `npm run watch` warning of missing dependencies for bluebird, winston, deep-freeze, and others. I can open a separate issue for that and/or include the two lock files in this PR.~~

Please let me know if I'm on the right track or if there are areas to make improvements. Thanks! 🙏

EDIT: The comment about dependencies was my fault and unique to my local. Please ignore. 😅 

Patch:
diff --git a/packages/data-point/README.md b/packages/data-point/README.md
index d248688..cd1ea84 100644
--- a/packages/data-point/README.md
+++ b/packages/data-point/README.md
@@ -315,6 +315,7 @@ PathReducer is a `string` value that extracts a path from the current [Accumulat
 | *$.* | Reference to current `accumulator.value`. |
 | *$..* | Gives you full access to current `accumulator`. |
 | *$path* | Simple Object path notation to extract a path from current `accumulator.value`. |
+| *$path[]* | Object path notation with trailing `[]` to extract a value from an array of objects at the `accumulator.value` for each. |
 
 #### <a name="root-path">Root path $.</a>
 
@@ -393,6 +394,35 @@ dataPoint
 
 Example at: [examples/reducer-path.js](examples/reducer-path.js)
 
+#### <a name="object-map">Object Map</a>
+
+**Example:**
+
+Map an array by traverising object structures
+
+```js
+const DataPoint = require('data-point')
+const dataPoint = DataPoint.create()
+
+const input = [
+  a: {
+    b: 'Hello World'
+  },
+  a: {
+    b: 'Hello Solar System'
+  },
+  a: {
+    b: 'Hello Universe'
+  }
+]
+
+dataPoint
+  .transform('$a.b[]', input)
+  .then((acc) => {
+    assert.equal(acc.value, ['Hello World', 'Hello Solar System', 'Hello Universe'])
+  })
+```
+
 ### <a name="function-reducer">FunctionReducer</a>
 
 A FunctionReducer allows you to use a function to apply a transformation. There are a couple of ways you may write your FunctionReducer:
diff --git a/packages/data-point/examples/reducer-path.js b/packages/data-point/examples/reducer-path.js
index f6918d0..535f94b 100644
--- a/packages/data-point/examples/reducer-path.js
+++ b/packages/data-point/examples/reducer-path.js
@@ -4,10 +4,32 @@ const assert = require('assert')
 const input = {
   a: {
     b: 'Hello World'
-  }
+  },
+  c: [
+    {
+      d: {
+        e: 1
+      }
+    },
+    {
+      d: {
+        e: 2
+      }
+    },
+    {
+      d: {
+        e: 3
+      }
+    }
+  ]
 }
 
 dataPoint.transform('$a.b', input).then(acc => {
   assert.equal(acc.value, 'Hello World')
   console.log(acc.value)
 })
+
+dataPoint.transform('$d.e[]', input.c).then(acc => {
+  assert.deepEqual(acc.value, [1, 2, 3])
+  console.log(acc.value)
+})
diff --git a/packages/data-point/lib/reducer-path/factory.js b/packages/data-point/lib/reducer-path/factory.js
index b0518fa..058f113 100644
--- a/packages/data-point/lib/reducer-path/factory.js
+++ b/packages/data-point/lib/reducer-path/factory.js
@@ -14,6 +14,7 @@ function ReducerPath () {
   this.type = REDUCER_PATH
   this.name = ''
   this.castAs = []
+  this.asCollection = false
 }
 
 module.exports.ReducerPath = ReducerPath
@@ -34,7 +35,8 @@ function create (source) {
 
   const parts = source.split(':')
 
-  reducer.name = _.defaultTo(parts[0].substr(1), '.')
+  reducer.asCollection = source.slice(-2) === '[]'
+  reducer.name = _.defaultTo(parts[0].substr(1), '.').replace(/\[]$/, '')
   reducer.castAs = _.defaultTo(parts[1], '*')
   reducer.params = parts.slice(2)
 
diff --git a/packages/data-point/lib/resolve/reducer-path.js b/packages/data-point/lib/resolve/reducer-path.js
index 8c7fb24..a6f204a 100644
--- a/packages/data-point/lib/resolve/reducer-path.js
+++ b/packages/data-point/lib/resolve/reducer-path.js
@@ -10,7 +10,14 @@ const utils = require('../utils')
  * @param {string} jsonPath
  * @returns
  */
-function resolveObjectPath (acc, jsonPath) {
+function resolveObjectPath (acc, jsonPath, reducerPath) {
+  if (typeof reducerPath === 'undefined') {
+    reducerPath = {}
+  }
+
+  const type = reducerPath.type || ''
+  const asCollection = reducerPath.asCollection || false
+
   if (jsonPath === '.' || _.isEmpty(jsonPath)) {
     return acc.value
   }
@@ -19,6 +26,14 @@ function resolveObjectPath (acc, jsonPath) {
     return _.get(acc, jsonPath.slice(2))
   }
 
+  if (type === 'ReducerPath' && asCollection) {
+    if (!Array.isArray(acc.value)) {
+      return null
+    }
+
+    return _.map(acc.value, jsonPath)
+  }
+
   return _.get(acc.value, jsonPath)
 }
 
@@ -34,7 +49,7 @@ module.exports.resolveObjectPath = resolveObjectPath
  * @returns {Promise<Accumulator>}
  */
 function resolve (store, resolveReducer, accumulator, reducerPath) {
-  const value = resolveObjectPath(accumulator, reducerPath.name)
+  const value = resolveObjectPath(accumulator, reducerPath.name, reducerPath)
   return Promise.resolve(utils.set(accumulator, 'value', value))
 }
 


Test patch:
diff --git a/packages/data-point/lib/reducer-path/factory.test.js b/packages/data-point/lib/reducer-path/factory.test.js
index c345215..1c14373 100644
--- a/packages/data-point/lib/reducer-path/factory.test.js
+++ b/packages/data-point/lib/reducer-path/factory.test.js
@@ -27,4 +27,11 @@ describe('reducer/reducer-path#create', () => {
     expect(reducer.name).toBe('a')
     expect(reducer.castAs).toBe('foo.bar')
   })
+
+  it('path with asCollection', () => {
+    const reducer = factory.create('$foo.bar[]')
+    expect(reducer.name).toBe('foo.bar')
+    expect(reducer.castAs).toBe('*')
+    expect(reducer.asCollection).toBe(true)
+  })
 })
diff --git a/packages/data-point/lib/resolve/reducer-path.test.js b/packages/data-point/lib/resolve/reducer-path.test.js
index 8d327d6..85b881b 100644
--- a/packages/data-point/lib/resolve/reducer-path.test.js
+++ b/packages/data-point/lib/resolve/reducer-path.test.js
@@ -3,6 +3,7 @@
 
 const AccumulatorFactory = require('../accumulator/factory')
 const reducerFactory = require('../reducer/factory')
+const reducerPathFactory = require('../reducer-path/factory')
 const reducerPath = require('./reducer-path')
 const resolveReducer = require('./reducer')
 
@@ -41,6 +42,93 @@ describe('resolve#reducer-path.resolveObjectPath', () => {
     expect(result).toBe('test')
   })
 
+  test('resolve valid collection path to resolved value', () => {
+    const acc = {
+      value: [
+        {
+          a: {
+            b: {
+              c: 1
+            }
+          }
+        },
+        {
+          a: {
+            b: {
+              c: 2
+            }
+          }
+        },
+        {
+          a: {
+            b: {
+              c: 3
+            }
+          }
+        }
+      ]
+    }
+    const reducerPathInstance = reducerPathFactory.create('$a.b.c[]')
+    const result = reducerPath.resolveObjectPath(
+      acc,
+      reducerPathInstance.name,
+      reducerPathInstance
+    )
+    expect(result).toEqual([1, 2, 3])
+  })
+
+  test('resolve invalid collection path key to array of undefined', () => {
+    const acc = {
+      value: [
+        {
+          a: {
+            b: {
+              c: 1
+            }
+          }
+        },
+        {
+          a: {
+            b: {
+              c: 2
+            }
+          }
+        },
+        {
+          a: {
+            b: {
+              c: 3
+            }
+          }
+        }
+      ]
+    }
+    const reducerPathInstance = reducerPathFactory.create('$a.b.d[]')
+    const result = reducerPath.resolveObjectPath(
+      acc,
+      reducerPathInstance.name,
+      reducerPathInstance
+    )
+    expect(result).toEqual([undefined, undefined, undefined])
+  })
+
+  test('resolve invalid collection path to null', () => {
+    const acc = {
+      value: {
+        a: {
+          b: 'c'
+        }
+      }
+    }
+    const reducerPathInstance = reducerPathFactory.create('$a.b.c[]')
+    const result = reducerPath.resolveObjectPath(
+      acc,
+      reducerPathInstance.name,
+      reducerPathInstance
+    )
+    expect(result).toBe(null)
+  })
+
   test('resolve prefixe ".." with valid jsonpath to resolved value', () => {
     const acc = {
       value: {
diff --git a/packages/data-point/lib/transform-expression/factory.test.js b/packages/data-point/lib/transform-expression/factory.test.js
index 4e927e1..7d7ff2c 100644
--- a/packages/data-point/lib/transform-expression/factory.test.js
+++ b/packages/data-point/lib/transform-expression/factory.test.js
@@ -46,6 +46,17 @@ describe('TransformFactory#create', () => {
     const reducer = _.first(result.reducers)
     expect(reducer.type).toBe('ReducerPath')
     expect(reducer.name).toBe('foo.bar')
+    expect(reducer.asCollection).toBe(false)
+  })
+
+  test('TransformFactory#create path as collection', () => {
+    const result = TransformFactory.create('$foo.bar[]')
+    expect(result.reducers).toHaveLength(1)
+
+    const reducer = _.first(result.reducers)
+    expect(reducer.type).toBe('ReducerPath')
+    expect(reducer.name).toBe('foo.bar')
+    expect(reducer.asCollection).toBe(true)
   })
 
   test('TransformFactory#create context with reducers', () => {