Repository: ViacomInc/data-point
Pull request #79 (2017-12-23 13:00:56)

Object reducer support
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**: #53 

<!-- Why are these changes necessary? -->
**Why**: This makes it easier to transform objects without writing a bunch of hash entities.

<!-- How were these changes implemented? -->
**How**: Adds a new ReducerObject type. This type is also used internally when `hash` entities are resolving `addKeys` and `mapKeys`.

<!-- 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" -->
- [x] Documentation
- [x] Tests
- [x] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [x] Added username to **all-contributors** list

<!-- feel free to add additional comments -->


raingerber: Working to fix these merge conflicts.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/14786607/badge)](https://coveralls.io/builds/14786607)

Coverage remained the same at 100.0% when pulling **f2b7e416654999192f89496bebb65e010533fb9e on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14786657/badge)](https://coveralls.io/builds/14786657)

Coverage remained the same at 100.0% when pulling **65647d77385b9b70fbe3c91e1b143d18186ff579 on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14786714/badge)](https://coveralls.io/builds/14786714)

Coverage remained the same at 100.0% when pulling **ea2ec085f29773ffc662c3305c4717e37ea5c7b4 on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14793664/badge)](https://coveralls.io/builds/14793664)

Coverage remained the same at 100.0% when pulling **ee64ff0c98e45c3b62448918ab7a4c83f92dcc4d on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14793670/badge)](https://coveralls.io/builds/14793670)

Coverage remained the same at 100.0% when pulling **ee64ff0c98e45c3b62448918ab7a4c83f92dcc4d on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14845880/badge)](https://coveralls.io/builds/14845880)

Coverage remained the same at 100.0% when pulling **66c6ccd65e4996ac5e7e7b67a40fc93507fb8e65 on raingerber:object-reducer-support** into **28b6ed9766164eddbc455dff5f4a86c17d930bfc on ViacomInc:master**.


Patch:
diff --git a/packages/data-point/README.md b/packages/data-point/README.md
index ad95580..1d011c6 100644
--- a/packages/data-point/README.md
+++ b/packages/data-point/README.md
@@ -27,6 +27,7 @@ npm install --save data-point
   - [Accumulator](#accumulator)
   - [PathReducer](#path-reducer)
   - [FunctionReducer](#function-reducer)
+  - [ObjectReducer](#object-reducer)
   - [Higher Order Reducers](#higher-order-reducers)
   - [Chained Reducers](#chained-reducers)
   - [EntityReducer](#entity-reducer)
@@ -85,7 +86,7 @@ Example at: [examples/hello-world.js](examples/hello-world.js)
 
 ## Async example
 
-Using [swapi.co](https://swapi.co) amazing service, the script below will get information about a planet and the residents of that planet.
+Using the amazing [swapi.co](https://swapi.co) service, the script below gets information about a planet and the residents of that planet.
 
 ```js
 const DataPoint = require('data-point')
@@ -279,7 +280,8 @@ DataPoint supports the following reducer types:
 
 1. [PathReducer](#path-reducer)
 2. [FunctionReducer](#function-reducer)
-3. [EntityReducer](#entity-reducer)
+3. [ObjectReducer](#object-reducer)
+4. [EntityReducer](#entity-reducer)
 
 ### <a name="accumulator">Accumulator</a>
 
@@ -562,6 +564,141 @@ dataPoint
 
 Example at: [examples/reducer-function-error.js](examples/reducer-function-error.js)
 
+### <a name="object-reducer">ObjectReducer</a>
+
+ObjectReducers are plain objects where the values are TransformExpressions. They're used to aggregate data or transform objects.
+
+**Transforming an object:**
+
+```js
+const inputData = {
+  x: {
+    y: {
+      z: 2
+    }
+  }
+}
+
+const objectReducer = {
+  y: '$x.y',
+  zPlusOne: ['$x.y.z', (acc) => acc.value + 1]
+}
+
+// output from dataPoint.transform(objectReducer, inputData):
+
+{
+  y: {
+    z: 2
+  },
+  zPlusOne: 3 
+}
+```
+
+**Combining multiple requests:**
+
+```js
+const dataPoint = require('data-point').create()
+
+dataPoint.addEntities({
+  'request:Planet': {
+    url: 'https://swapi.co/api/planets/{value}'
+  }
+})
+
+const objectReducer = {
+  tatooine: ['$tatooine', 'request:Planet'],
+  alderaan: ['$alderaan', 'request:Planet']
+}
+
+const planetIds = {
+  tatooine: 1,
+  alderaan: 2
+}
+
+dataPoint.transform(objectReducer, planetIds)
+  .then(acc => {
+    // do something with the aggregated planet data!
+  })
+
+```
+
+Each of the TransformExpressions, including the nested ones, are resolved against the same accumulator value. This means that input objects can be rearranged at any level:
+
+```js
+const inputData = {
+  a: 'A',
+  b: 'B',
+  c: {
+    x: 'X',
+    y: 'Y'
+  }
+})
+
+// some data will move to a higher level of nesting,
+// but other data will move deeper into the object
+const objectReducer = {
+  x: '$c.x',
+  y: '$c.y',
+  z: {
+    a: '$a',
+    b: '$b'
+  }
+}
+
+// output from dataPoint.transform(objectReducer, inputData):
+
+{
+  x: 'X',
+  y: 'Y',
+  z: {
+    a: 'A',
+    b: 'B'
+  }
+}
+```
+
+Each of the TransformExpressions might also contain more ObjectReducers (which might contain TransformExpressions, and so on). Notice how the output changes based on the position of the ObjectReducers in the two expressions:
+
+```js
+const inputData = {
+  a: {
+    a: 1,
+    b: 2
+  }
+}
+
+const objectReducer = {
+  x: [
+    '$a',
+    // this comes second, so it's resolved
+    // against the output from the '$a' transform
+    {
+      a: '$a'
+    }
+  ],
+  y: [
+    // this comes first, so it's resolved
+    // against the main input to objectReducer
+    {
+      a: '$a'
+    },
+    '$a'
+  ]
+}
+
+// output from dataPoint.transform(objectReducer, inputData):
+
+{
+  x: {
+    a: 1
+  },
+  y: {
+    a: 1,
+    b: 2
+  }
+}
+```
+
 ### <a name="higher-order-reducers">Higher Order Reducers</a>
 
 Higher Order Reducers are expected to be [Higher-order Functions](https://en.wikipedia.org/wiki/Higher-order_function). This means that a higher order reducer **MUST** return a [FunctionReducer](#function-reducer).
diff --git a/packages/data-point/examples/reducer-object-1.js b/packages/data-point/examples/reducer-object-1.js
new file mode 100644
index 0000000..2adf522
--- /dev/null
+++ b/packages/data-point/examples/reducer-object-1.js
@@ -0,0 +1,18 @@
+const dataPoint = require('../').create()
+
+const reducer = {
+  y: '$x.y',
+  zPlusOne: ['$x.y.z', acc => acc.value + 1]
+}
+
+const data = {
+  x: {
+    y: {
+      z: 2
+    }
+  }
+}
+
+dataPoint.transform(reducer, data).then(acc => {
+  console.log(acc.value)
+})
diff --git a/packages/data-point/examples/reducer-object-2.js b/packages/data-point/examples/reducer-object-2.js
new file mode 100644
index 0000000..fdb908c
--- /dev/null
+++ b/packages/data-point/examples/reducer-object-2.js
@@ -0,0 +1,23 @@
+const dataPoint = require('../').create()
+
+const reducer = {
+  x: '$c.x',
+  y: '$c.y',
+  z: {
+    a: '$a',
+    b: '$b'
+  }
+}
+
+const data = {
+  a: 'A',
+  b: 'B',
+  c: {
+    x: 'X',
+    y: 'Y'
+  }
+}
+
+dataPoint.transform(reducer, data).then(acc => {
+  console.log(acc.value)
+})
diff --git a/packages/data-point/examples/reducer-object-3.js b/packages/data-point/examples/reducer-object-3.js
new file mode 100644
index 0000000..1da6982
--- /dev/null
+++ b/packages/data-point/examples/reducer-object-3.js
@@ -0,0 +1,27 @@
+const dataPoint = require('../').create()
+
+const reducer = {
+  x: [
+    '$a',
+    {
+      a: '$a'
+    }
+  ],
+  y: [
+    {
+      a: '$a'
+    },
+    '$a'
+  ]
+}
+
+const data = {
+  a: {
+    a: 1,
+    b: 2
+  }
+}
+
+dataPoint.transform(reducer, data).then(acc => {
+  console.log(acc.value)
+})
diff --git a/packages/data-point/lib/entity-types/entity-control/reducer.js b/packages/data-point/lib/entity-types/entity-control/reducer.js
index 2fe1a9a..1df66e9 100644
--- a/packages/data-point/lib/entity-types/entity-control/reducer.js
+++ b/packages/data-point/lib/entity-types/entity-control/reducer.js
@@ -41,7 +41,7 @@ module.exports.getMatchingCaseStatement = getMatchingCaseStatement
 function resolve (acc, resolveTransform) {
   const selectControl = acc.reducer.spec.select
   const caseStatements = selectControl.cases
-  const defaultTransfrom = selectControl.default
+  const defaultTransform = selectControl.default
 
   return getMatchingCaseStatement(caseStatements, acc, resolveTransform).then(
     caseStatement => {
@@ -49,7 +49,7 @@ function resolve (acc, resolveTransform) {
         return resolveTransform(acc, caseStatement.do)
       }
 
-      return resolveTransform(acc, defaultTransfrom)
+      return resolveTransform(acc, defaultTransform)
     }
   )
 }
diff --git a/packages/data-point/lib/entity-types/entity-hash/factory.js b/packages/data-point/lib/entity-types/entity-hash/factory.js
index 385f67e..fae882b 100644
--- a/packages/data-point/lib/entity-types/entity-hash/factory.js
+++ b/packages/data-point/lib/entity-types/entity-hash/factory.js
@@ -2,6 +2,7 @@
 
 const _ = require('lodash')
 const createTransform = require('../../transform-expression').create
+const createReducerObject = require('../../reducer-object').create
 const deepFreeze = require('deep-freeze')
 const parseCompose = require('../parse-compose')
 const helpers = require('../../helpers')
@@ -15,22 +16,10 @@ module.exports.EntityHash = EntityHash
 
 const modifierKeys = ['omitKeys', 'pickKeys', 'mapKeys', 'addValues', 'addKeys']
 
-function parseCollectionKeys (object, parseCallback) {
-  const result = {}
-
-  _.forOwn(object, (value, key) => {
-    result[key] = parseCallback(object[key])
-  })
-
-  return result
-}
-
-module.exports.parseCollectionKeys = parseCollectionKeys
-
 function createCompose (composeParse) {
   return composeParse.map(modifier => {
-    let transform
     let spec
+    let transform
     switch (modifier.type) {
       case 'addValues':
         spec = _.defaultTo(modifier.spec, {})
@@ -43,8 +32,7 @@ function createCompose (composeParse) {
         break
       case 'mapKeys':
       case 'addKeys':
-        spec = _.defaultTo(modifier.spec, {})
-        transform = parseCollectionKeys(spec, createTransform)
+        transform = createReducerObject(createTransform, modifier.spec)
     }
     return _.assign({}, modifier, {
       transform
diff --git a/packages/data-point/lib/entity-types/entity-hash/reducer.js b/packages/data-point/lib/entity-types/entity-hash/reducer.js
index a976e53..41ecda9 100644
--- a/packages/data-point/lib/entity-types/entity-hash/reducer.js
+++ b/packages/data-point/lib/entity-types/entity-hash/reducer.js
@@ -4,48 +4,28 @@ const _ = require('lodash')
 const Promise = require('bluebird')
 const utils = require('../../utils')
 
-function flattenResult (key) {
-  return function flatten (result) {
-    return {
-      key,
-      value: result.value
-    }
-  }
+function resolveMapKeys (accumulator, reducer, resolveTransform) {
+  const transform = { reducers: [reducer] }
+  return resolveTransform(accumulator, transform).then(acc => {
+    return utils.set(accumulator, 'value', acc.value)
+  })
 }
 
-function resolveMapKeys (accumulator, modelMap, resolveTransform) {
-  const keyPairs = _.toPairs(modelMap)
+module.exports.resolveMapKeys = resolveMapKeys
 
-  // exit if no keys to map
-  if (keyPairs.length === 0) {
-    return Promise.resolve(accumulator)
-  }
-
-  const resolvedMap = Promise.resolve(keyPairs)
-    .map(keyPair => {
-      const key = keyPair[0]
-      const transform = keyPair[1]
-      return resolveTransform(accumulator, transform).then(flattenResult(key))
-    })
-    .then(result => {
-      const resultMap = result.reduce((acc, item) => {
-        /* eslint no-param-reassign: "off" */
-        acc[item.key] = item.value
-        return acc
-      }, {})
-
-      return resultMap
-    })
-    .then(result => utils.set(accumulator, 'value', result))
-
-  return resolvedMap
+function resolveAddKeys (accumulator, reducer, resolveTransform) {
+  const transform = { reducers: [reducer] }
+  return resolveTransform(accumulator, transform).then(acc => {
+    return resolveAddValues(accumulator, acc.value)
+  })
 }
 
+module.exports.resolveAddKeys = resolveAddKeys
+
 function resolveAddValues (accumulator, modelValues) {
   if (Object.keys(modelValues).length === 0) {
     return Promise.resolve(accumulator)
   }
-
   const value = _.assign({}, accumulator.value, modelValues)
   const resolvedReducer = utils.set(accumulator, 'value', value)
   return Promise.resolve(resolvedReducer)
@@ -53,18 +33,6 @@ function resolveAddValues (accumulator, modelValues) {
 
 module.exports.resolveAddValues = resolveAddValues
 
-function resolveAddKeys (accumulator, modelExtend, resolveTransform) {
-  if (Object.keys(modelExtend).length === 0) {
-    return Promise.resolve(accumulator)
-  }
-
-  return resolveMapKeys(accumulator, modelExtend, resolveTransform).then(
-    result => resolveAddValues(accumulator, result.value)
-  )
-}
-
-module.exports.resolveAddKeys = resolveAddKeys
-
 function resolveOmitKeys (accumulator, modelOmit) {
   if (modelOmit.length === 0) {
     return Promise.resolve(accumulator)
@@ -90,25 +58,27 @@ module.exports.resolvePickKeys = resolvePickKeys
 // NOTE: as expensive as this might be, this is to avoid 'surprises'
 function validateAsObject (acc) {
   const entity = acc.reducer.spec
-  return !_.isPlainObject(acc.value)
-    ? Promise.reject(
-      new Error(
-        `"${entity.id}" received acc.value = ${JSON.stringify(
-          acc.value
-        ).substr(0, 15)} of type ${utils.typeOf(
-          acc.value
-        )} this entity only process plain Objects. More info https://github.com/ViacomInc/data-point#hash-entity`
-      )
+  if (_.isPlainObject(acc.value)) {
+    return acc
+  }
+  return Promise.reject(
+    new Error(
+      `"${entity.id}" received acc.value = ${JSON.stringify(acc.value).substr(
+        0,
+        15
+      )} of type ${utils.typeOf(
+        acc.value
+      )} this entity only processes plain Objects. More info https://github.com/ViacomInc/data-point#hash-entity`
     )
-    : acc
+  )
 }
 
 const modifierFunctionMap = {
+  omitKeys: resolveOmitKeys,
+  pickKeys: resolvePickKeys,
   mapKeys: resolveMapKeys,
-  addKeys: resolveAddKeys,
   addValues: resolveAddValues,
-  omitKeys: resolveOmitKeys,
-  pickKeys: resolvePickKeys
+  addKeys: resolveAddKeys
 }
 
 function resolveCompose (accumulator, composeReducers, resolveTransform) {
diff --git a/packages/data-point/lib/reducer-entity/factory.js b/packages/data-point/lib/reducer-entity/factory.js
index 4376066..87ae800 100644
--- a/packages/data-point/lib/reducer-entity/factory.js
+++ b/packages/data-point/lib/reducer-entity/factory.js
@@ -1,6 +1,10 @@
 'use strict'
+
 const _ = require('lodash')
-const REDUCER_ENTITY = (module.exports.type = 'ReducerEntity')
+
+const REDUCER_ENTITY = 'ReducerEntity'
+
+module.exports.type = REDUCER_ENTITY
 
 /**
  * Defines a entity reducer
diff --git a/packages/data-point/lib/reducer-function/factory.js b/packages/data-point/lib/reducer-function/factory.js
index 71c099b..e3f9b3e 100644
--- a/packages/data-point/lib/reducer-function/factory.js
+++ b/packages/data-point/lib/reducer-function/factory.js
@@ -2,7 +2,9 @@
 
 const _ = require('lodash')
 
-const REDUCER_FUNCTION = (module.exports.type = 'ReducerFunction')
+const REDUCER_FUNCTION = 'ReducerFunction'
+
+module.exports.type = REDUCER_FUNCTION
 
 /**
  * @class
diff --git a/packages/data-point/lib/reducer-object/factory.js b/packages/data-point/lib/reducer-object/factory.js
new file mode 100644
index 0000000..223beb3
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/factory.js
@@ -0,0 +1,74 @@
+const isPlainObject = require('lodash/isPlainObject')
+
+const REDUCER_OBJECT = 'ReducerObject'
+
+module.exports.type = REDUCER_OBJECT
+
+/**
+ * @class
+ * @property {string} type - @see reducerType
+ * @property {Array} props
+ */
+function ReducerObject () {
+  this.type = REDUCER_OBJECT
+  this.props = undefined
+}
+
+module.exports.ReducerObject = ReducerObject
+
+/**
+ * @param {*} source
+ * @returns {boolean}
+ */
+function isObject (source) {
+  return isPlainObject(source)
+}
+
+module.exports.isObject = isObject
+
+/**
+ * @param {Function} createTransform
+ * @param {Object} source
+ * @param {Array} path
+ * @param {Array} reducerProps
+ * @returns {Array}
+ */
+function getReducerProps (
+  createTransform,
+  source,
+  path = [],
+  reducerProps = []
+) {
+  for (let key of Object.keys(source)) {
+    const value = source[key]
+    const fullPath = path.concat(key)
+    if (isPlainObject(value)) {
+      // NOTE: recursive call
+      getReducerProps(createTransform, value, fullPath, reducerProps)
+      continue
+    }
+
+    reducerProps.push({
+      path: fullPath,
+      transform: createTransform(value)
+    })
+  }
+
+  return reducerProps
+}
+
+module.exports.getReducerProps = getReducerProps
+
+/**
+ * @param {Function} createTransform
+ * @param {Object} source
+ * @returns {reducer}
+ */
+function create (createTransform, source = {}) {
+  const reducer = new ReducerObject()
+  reducer.props = getReducerProps(createTransform, source)
+
+  return Object.freeze(reducer)
+}
+
+module.exports.create = create
diff --git a/packages/data-point/lib/reducer-object/index.js b/packages/data-point/lib/reducer-object/index.js
new file mode 100644
index 0000000..4c6c98b
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/index.js
@@ -0,0 +1,11 @@
+/* eslint global-require: 0 */
+
+const reducer = require('./factory')
+const resolve = require('./resolve').resolve
+
+module.exports = {
+  create: reducer.create,
+  type: reducer.type,
+  isType: reducer.isObject,
+  resolve: resolve
+}
diff --git a/packages/data-point/lib/reducer-object/resolve.js b/packages/data-point/lib/reducer-object/resolve.js
new file mode 100644
index 0000000..c8fb99a
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/resolve.js
@@ -0,0 +1,28 @@
+const Promise = require('bluebird')
+const set = require('lodash/set')
+const utils = require('../utils')
+
+/**
+ * @param {Object} store
+ * @param {Function} resolveTransform
+ * @param {Accumulator} accumulator
+ * @param {ReducerObject} reducer
+ * @returns {Promise<Accumulator>}
+ */
+function resolve (store, resolveTransform, accumulator, reducer) {
+  if (reducer.props.length === 0) {
+    return Promise.resolve(accumulator)
+  }
+
+  const props = Promise.map(reducer.props, ({ path, transform }) => {
+    return resolveTransform(store, accumulator, transform).then(
+      ({ value }) => ({ path, value })
+    )
+  })
+
+  return props
+    .reduce((acc, { path, value }) => set(acc, path, value), {})
+    .then(value => utils.set(accumulator, 'value', value))
+}
+
+module.exports.resolve = resolve
diff --git a/packages/data-point/lib/reducer-path/factory.js b/packages/data-point/lib/reducer-path/factory.js
index 685105f..f517de0 100644
--- a/packages/data-point/lib/reducer-path/factory.js
+++ b/packages/data-point/lib/reducer-path/factory.js
@@ -2,7 +2,9 @@
 
 const _ = require('lodash')
 
-const REDUCER_PATH = (module.exports.type = 'ReducerPath')
+const REDUCER_PATH = 'ReducerPath'
+
+module.exports.type = REDUCER_PATH
 
 /**
  * @class
diff --git a/packages/data-point/lib/reducer/factory.js b/packages/data-point/lib/reducer/factory.js
index b73ecce..7162ba8 100644
--- a/packages/data-point/lib/reducer/factory.js
+++ b/packages/data-point/lib/reducer/factory.js
@@ -4,6 +4,7 @@ const _ = require('lodash')
 
 const ReducerPath = require('../reducer-path')
 const ReducerFunction = require('../reducer-function')
+const ReducerObject = require('../reducer-object')
 const ReducerEntity = require('../reducer-entity')
 const util = require('util')
 
@@ -14,7 +15,13 @@ const reducerTypes = [ReducerPath, ReducerFunction, ReducerEntity]
  * @param  {string} source - reducer string representation
  * @return {reducer}
  */
-function create (source) {
+function create (createTransform, source) {
+  // ReducerObject requires an extra parameter, so
+  // it's not included in the reducerTypes array
+  if (ReducerObject.isType(source)) {
+    return ReducerObject.create(createTransform, source)
+  }
+
   const reducer = _.find(reducerTypes, r => r.isType(source))
 
   if (_.isUndefined(reducer)) {
diff --git a/packages/data-point/lib/transform-expression/factory.js b/packages/data-point/lib/transform-expression/factory.js
index 316abfc..85d79f5 100644
--- a/packages/data-point/lib/transform-expression/factory.js
+++ b/packages/data-point/lib/transform-expression/factory.js
@@ -39,11 +39,7 @@ module.exports.parseFromArray = parseFromArray
 
 function parse (src) {
   let source = _.defaultTo(src, [])
-
-  if (_.isString(src) || _.isFunction(src)) {
-    source = [src]
-  }
-
+  source = _.castArray(source)
   return parseFromArray(source)
 }
 
@@ -51,7 +47,12 @@ module.exports.parse = parse
 
 function isValid (source) {
   const type = typeof source
-  return type === 'string' || type === 'function' || source instanceof Array
+  return (
+    type === 'string' ||
+    type === 'function' ||
+    source instanceof Array ||
+    _.isPlainObject(source)
+  )
 }
 
 module.exports.isValid = isValid
@@ -64,7 +65,7 @@ function validate (source) {
   const message = [
     `Could not parse a TransformExpression. The TransformExpression:\n `,
     _.attempt(util.inspect, source),
-    '\nis not using a valid type, try using an Array, String or a Function.',
+    '\nis not using a valid type, try using an Array, String, Object, or Function.',
     '\nMore info: https://github.com/ViacomInc/data-point/tree/master/packages/data-point#transform-expression\n'
   ].join('')
   throw new Error(message)
@@ -82,7 +83,11 @@ function create (source = []) {
   const tokens = parse(source)
 
   const transformBase = new TransformExpression()
-  transformBase.reducers = tokens.map(reducerFactory.create)
+
+  transformBase.reducers = tokens.map(token => {
+    // NOTE: recursive call
+    return reducerFactory.create(create, token)
+  })
 
   return Object.freeze(transformBase)
 }
diff --git a/packages/data-point/lib/transform-expression/resolve.js b/packages/data-point/lib/transform-expression/resolve.js
index a8b872b..2e248e1 100644
--- a/packages/data-point/lib/transform-expression/resolve.js
+++ b/packages/data-point/lib/transform-expression/resolve.js
@@ -4,11 +4,10 @@ const _ = require('lodash')
 const Promise = require('bluebird')
 const resolveReducerPath = require('../reducer-path').resolve
 const resolveReducerFunction = require('../reducer-function').resolve
+const resolveReducerObject = require('../reducer-object').resolve
 const resolveReducerEntity = require('../reducer-entity').resolve
 
 /**
- *
- *
  * @param {Object} store
  * @param {any} reducerType
  * @returns
@@ -24,6 +23,9 @@ function getReducerFunction (store, reducerType) {
     case 'ReducerFunction':
       reducerResolver = resolveReducerFunction
       break
+    case 'ReducerObject':
+      reducerResolver = _.partial(resolveReducerObject, store, resolve)
+      break
     case 'ReducerEntity':
       /* eslint no-use-before-define: "off" */
       // IMPORTANT: recursiveness here - watch out!


Test patch:
diff --git a/packages/data-point/lib/entity-types/entity-hash/factory.test.js b/packages/data-point/lib/entity-types/entity-hash/factory.test.js
index 5147ca1..39dab68 100644
--- a/packages/data-point/lib/entity-types/entity-hash/factory.test.js
+++ b/packages/data-point/lib/entity-types/entity-hash/factory.test.js
@@ -24,10 +24,7 @@ describe('factory#parse loose modifiers', () => {
     })
 
     expect(result.compose[0]).toHaveProperty('type', 'mapKeys')
-    expect(result.compose[0].transform.a).toHaveProperty(
-      'typeOf',
-      'TransformExpression'
-    )
+    expect(result.compose[0].transform).toHaveProperty('type', 'ReducerObject')
   })
   test('factory#addKeys', () => {
     const result = factory.create({
@@ -37,10 +34,7 @@ describe('factory#parse loose modifiers', () => {
     })
 
     expect(result.compose[0]).toHaveProperty('type', 'addKeys')
-    expect(result.compose[0].transform.a).toHaveProperty(
-      'typeOf',
-      'TransformExpression'
-    )
+    expect(result.compose[0].transform).toHaveProperty('type', 'ReducerObject')
   })
   test('factory#omitKeys', () => {
     const result = factory.create({
@@ -109,10 +103,7 @@ describe('factory#parse composed modifiers', () => {
     })
 
     expect(result.compose[0]).toHaveProperty('type', 'mapKeys')
-    expect(result.compose[0].transform.a).toHaveProperty(
-      'typeOf',
-      'TransformExpression'
-    )
+    expect(result.compose[0].transform).toHaveProperty('type', 'ReducerObject')
   })
   test('factory#multiple modifiers', () => {
     const result = factory.create({
@@ -134,12 +125,16 @@ describe('factory#parse composed modifiers', () => {
     })
 
     expect(result.compose[0]).toHaveProperty('type', 'addKeys')
-    expect(result.compose[0].transform.a).toHaveProperty(
+    expect(result.compose[0].transform).toHaveProperty('type', 'ReducerObject')
+    expect(result.compose[0].transform.props[0].path).toEqual(['a'])
+    expect(result.compose[0].transform.props[0].transform).toHaveProperty(
       'typeOf',
       'TransformExpression'
     )
     expect(result.compose[1]).toHaveProperty('type', 'mapKeys')
-    expect(result.compose[1].transform.a).toHaveProperty(
+    expect(result.compose[1].transform).toHaveProperty('type', 'ReducerObject')
+    expect(result.compose[1].transform.props[0].path).toEqual(['a'])
+    expect(result.compose[1].transform.props[0].transform).toHaveProperty(
       'typeOf',
       'TransformExpression'
     )
diff --git a/packages/data-point/lib/entity-types/entity-request/reducer.test.js b/packages/data-point/lib/entity-types/entity-request/reducer.test.js
index 45bea6b..a0064e8 100644
--- a/packages/data-point/lib/entity-types/entity-request/reducer.test.js
+++ b/packages/data-point/lib/entity-types/entity-request/reducer.test.js
@@ -7,6 +7,7 @@ const Reducer = require('./reducer')
 
 const AccumulatorFactory = require('../../accumulator/factory')
 const ReducerFactory = require('../../reducer/factory')
+const createTransform = require('../../transform-expression/factory').create
 const LocalsFactory = require('../../locals/factory')
 
 const ResolveEntity = require('../resolve-entity')
@@ -38,7 +39,7 @@ function helperMockContext (accumulatorData, reducerSource, requestName) {
     locals,
     values
   })
-  const reducer = ReducerFactory.create(reducerSource)
+  const reducer = ReducerFactory.create(createTransform, reducerSource)
   return ResolveEntity.createCurrentAccumulator(dataPoint, accumulator, reducer)
 }
 
diff --git a/packages/data-point/lib/reducer-function/resolve.test.js b/packages/data-point/lib/reducer-function/resolve.test.js
index 324193f..dc36ccb 100644
--- a/packages/data-point/lib/reducer-function/resolve.test.js
+++ b/packages/data-point/lib/reducer-function/resolve.test.js
@@ -4,6 +4,7 @@
 const AccumulatorFactory = require('../accumulator/factory')
 const reducerFactory = require('../reducer/factory')
 const resolveFunction = require('./resolve')
+const createTransform = require('../transform-expression').create
 
 describe('resolve#filter.resolve', () => {
   test('resolves node style callback', () => {
@@ -11,7 +12,7 @@ describe('resolve#filter.resolve', () => {
       value: 'test'
     })
 
-    const reducer = reducerFactory.create((acc, done) =>
+    const reducer = reducerFactory.create(createTransform, (acc, done) =>
       done(null, `${acc.value}node`)
     )
 
@@ -25,7 +26,10 @@ describe('resolve#filter.resolve', () => {
       value: 'test'
     })
 
-    const reducer = reducerFactory.create(acc => `${acc.value}sync`)
+    const reducer = reducerFactory.create(
+      createTransform,
+      acc => `${acc.value}sync`
+    )
 
     return resolveFunction.resolve(accumulator, reducer).then(result => {
       expect(result.value).toBe('testsync')
@@ -37,7 +41,7 @@ describe('resolve#filter.resolve', () => {
       value: 'test'
     })
 
-    const reducer = reducerFactory.create(acc =>
+    const reducer = reducerFactory.create(createTransform, acc =>
       Promise.resolve(`${acc.value}promise`)
     )
 
@@ -51,7 +55,7 @@ describe('resolve#filter.resolve', () => {
       value: 'test'
     })
 
-    const reducer = reducerFactory.create((acc, done) => {
+    const reducer = reducerFactory.create(createTransform, (acc, done) => {
       return done(new Error('Test'))
     })
 
diff --git a/packages/data-point/lib/reducer-object/__snapshots__/factory.test.js.snap b/packages/data-point/lib/reducer-object/__snapshots__/factory.test.js.snap
new file mode 100644
index 0000000..ef2a774
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/__snapshots__/factory.test.js.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`reducer/reducer-function#getReducerProps should accept a non-empty object 1`] = `
+Array [
+  Object {
+    "path": Array [
+      "a1",
+    ],
+    "transform": TransformExpression {
+      "reducers": Array [
+        ReducerPath {
+          "asCollection": true,
+          "body": [Function],
+          "name": "a",
+          "type": "ReducerPath",
+        },
+      ],
+      "typeOf": "TransformExpression",
+    },
+  },
+  Object {
+    "path": Array [
+      "b1",
+      "a2",
+    ],
+    "transform": TransformExpression {
+      "reducers": Array [
+        ReducerPath {
+          "asCollection": false,
+          "body": [Function],
+          "name": "a2",
+          "type": "ReducerPath",
+        },
+        ReducerFunction {
+          "body": [Function],
+          "type": "ReducerFunction",
+        },
+      ],
+      "typeOf": "TransformExpression",
+    },
+  },
+  Object {
+    "path": Array [
+      "b1",
+      "b2",
+    ],
+    "transform": TransformExpression {
+      "reducers": Array [
+        ReducerObject {
+          "props": Array [
+            Object {
+              "path": Array [
+                "a3",
+              ],
+              "transform": TransformExpression {
+                "reducers": Array [
+                  ReducerEntity {
+                    "asCollection": false,
+                    "entityType": "transform",
+                    "name": "entity-name",
+                    "type": "ReducerEntity",
+                  },
+                ],
+                "typeOf": "TransformExpression",
+              },
+            },
+            Object {
+              "path": Array [
+                "b3",
+              ],
+              "transform": TransformExpression {
+                "reducers": Array [
+                  ReducerPath {
+                    "asCollection": false,
+                    "body": [Function],
+                    "name": "b1.b2.b3",
+                    "type": "ReducerPath",
+                  },
+                ],
+                "typeOf": "TransformExpression",
+              },
+            },
+          ],
+          "type": "ReducerObject",
+        },
+      ],
+      "typeOf": "TransformExpression",
+    },
+  },
+]
+`;
diff --git a/packages/data-point/lib/reducer-object/factory.test.js b/packages/data-point/lib/reducer-object/factory.test.js
new file mode 100644
index 0000000..3ceea19
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/factory.test.js
@@ -0,0 +1,57 @@
+/* eslint-env jest */
+'use strict'
+
+const factory = require('./factory')
+const createTransform = require('../transform-expression').create
+
+test('reducer/reducer-function#isObject', () => {
+  expect(factory.isObject('$foo')).toBe(false)
+  expect(factory.isObject(() => true)).toBe(false)
+  expect(factory.isObject([])).toBe(false)
+  expect(factory.isObject({})).toBe(true)
+})
+
+describe('reducer/reducer-function#getReducerProps', () => {
+  it('should accept an empty object', () => {
+    const reducer = factory.getReducerProps(createTransform, {})
+    expect(reducer).toEqual([])
+  })
+  it('should accept a non-empty object', () => {
+    const reducer = factory.getReducerProps(createTransform, {
+      a1: '$a[]',
+      b1: {
+        a2: ['$a2', () => false],
+        b2: [
+          {
+            a3: 'transform:entity-name',
+            b3: '$b1.b2.b3'
+          }
+        ]
+      }
+    })
+    expect(reducer).toMatchSnapshot()
+  })
+})
+
+describe('reducer/reducer-path#create', () => {
+  it('reducer object with no props argument', () => {
+    const reducer = factory.create(createTransform)
+    expect(reducer.type).toBe('ReducerObject')
+    expect(reducer.props).toBeInstanceOf(Array)
+  })
+  it('reducer object with empty props argument', () => {
+    const reducer = factory.create(createTransform, {})
+    expect(reducer.type).toBe('ReducerObject')
+    expect(reducer.props).toBeInstanceOf(Array)
+  })
+  it('reducer object with props argument', () => {
+    const reducer = factory.create(createTransform, {
+      a: '$a',
+      b: '$b',
+      c: '$c'
+    })
+    expect(reducer.type).toBe('ReducerObject')
+    expect(reducer.props).toBeInstanceOf(Array)
+    expect(reducer.props).toHaveLength(3)
+  })
+})
diff --git a/packages/data-point/lib/reducer-object/resolve.test.js b/packages/data-point/lib/reducer-object/resolve.test.js
new file mode 100644
index 0000000..cb2fb47
--- /dev/null
+++ b/packages/data-point/lib/reducer-object/resolve.test.js
@@ -0,0 +1,152 @@
+/* eslint-env jest */
+'use strict'
+
+const AccumulatorFactory = require('../accumulator/factory')
+const reducerFactory = require('../reducer/factory')
+const resolveObject = require('./resolve').resolve
+const createTransform = require('../transform-expression').create
+const resolveTransform = require('../transform-expression').resolve
+const FixtureStore = require('../../test/utils/fixture-store')
+
+let dataPoint
+
+beforeAll(() => {
+  dataPoint = FixtureStore.create()
+})
+
+describe('resolve#reducerObject.resolve', () => {
+  it('should return the accumulator for an empty reducer object', () => {
+    const reducer = reducerFactory.create(createTransform, {})
+
+    const accumulator = AccumulatorFactory.create({
+      value: {
+        x: {
+          y: {
+            z: 2
+          }
+        }
+      }
+    })
+
+    return resolveObject(
+      dataPoint,
+      resolveTransform,
+      accumulator,
+      reducer
+    ).then(result => {
+      expect(result.value).toEqual(accumulator.value)
+    })
+  })
+
+  it('should resolve a reducer object', () => {
+    const reducer = reducerFactory.create(createTransform, {
+      y: '$x.y',
+      zPlusOne: ['$x.y.z', acc => acc.value + 1]
+    })
+
+    const accumulator = AccumulatorFactory.create({
+      value: {
+        x: {
+          y: {
+            z: 2
+          }
+        }
+      }
+    })
+
+    return resolveObject(
+      dataPoint,
+      resolveTransform,
+      accumulator,
+      reducer
+    ).then(result => {
+      expect(result.value).toEqual({
+        y: {
+          z: 2
+        },
+        zPlusOne: 3
+      })
+    })
+  })
+
+  it('should resolve a reducer object', () => {
+    const reducer = reducerFactory.create(createTransform, {
+      x: '$c.x',
+      y: '$c.y',
+      z: {
+        a: '$a',
+        b: '$b'
+      }
+    })
+
+    const accumulator = AccumulatorFactory.create({
+      value: {
+        a: 'A',
+        b: 'B',
+        c: {
+          x: 'X',
+          y: 'Y'
+        }
+      }
+    })
+
+    return resolveObject(
+      dataPoint,
+      resolveTransform,
+      accumulator,
+      reducer
+    ).then(result => {
+      expect(result.value).toEqual({
+        x: 'X',
+        y: 'Y',
+        z: {
+          a: 'A',
+          b: 'B'
+        }
+      })
+    })
+  })
+
+  it('should resolve a reducer object', () => {
+    const reducer = reducerFactory.create(createTransform, {
+      x: [
+        '$a',
+        {
+          a: '$a'
+        }
+      ],
+      y: [
+        {
+          a: '$a'
+        },
+        '$a'
+      ]
+    })
+
+    const accumulator = AccumulatorFactory.create({
+      value: {
+        a: {
+          a: 1,
+          b: 2
+        }
+      }
+    })
+
+    return resolveObject(
+      dataPoint,
+      resolveTransform,
+      accumulator,
+      reducer
+    ).then(result => {
+      expect(result.value).toEqual({
+        x: {
+          a: 1
+        },
+        y: {
+          a: 1,
+          b: 2
+        }
+      })
+    })
+  })
+})
diff --git a/packages/data-point/lib/reducer-path/resolve.test.js b/packages/data-point/lib/reducer-path/resolve.test.js
index f57231f..3e5ba71 100644
--- a/packages/data-point/lib/reducer-path/resolve.test.js
+++ b/packages/data-point/lib/reducer-path/resolve.test.js
@@ -4,18 +4,22 @@
 const AccumulatorFactory = require('../accumulator/factory')
 const reducerFactory = require('../reducer/factory')
 const reducerPath = require('../reducer-path')
+const createTransform = require('../transform-expression').create
 
 describe('resolve#transform.resolve', () => {
   function resolve (value, rawReducer) {
     const locals = {
       a: ['testA']
     }
-    const filtercontext = AccumulatorFactory.create({
+    const accumulator = AccumulatorFactory.create({
       value,
       locals
     })
 
-    return reducerPath.resolve(filtercontext, reducerFactory.create(rawReducer))
+    return reducerPath.resolve(
+      accumulator,
+      reducerFactory.create(createTransform, rawReducer)
+    )
   }
 
   test('resolve current value', () => {
diff --git a/packages/data-point/lib/reducer/__snapshots__/factory.test.js.snap b/packages/data-point/lib/reducer/__snapshots__/factory.test.js.snap
index 6b6b84d..1baa580 100644
--- a/packages/data-point/lib/reducer/__snapshots__/factory.test.js.snap
+++ b/packages/data-point/lib/reducer/__snapshots__/factory.test.js.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`reducer#create  detect invalid 1`] = `
+exports[`reducer#create detect invalid 1`] = `
 "Invalid reducer type. Could not find a matching reducer type while parsing the value:
  'a'
 For a list of supported types visit:
diff --git a/packages/data-point/lib/reducer/factory.test.js b/packages/data-point/lib/reducer/factory.test.js
index 2f2a8a7..d0431d8 100644
--- a/packages/data-point/lib/reducer/factory.test.js
+++ b/packages/data-point/lib/reducer/factory.test.js
@@ -5,31 +5,39 @@ const Factory = require('./factory')
 
 const ReducerPath = require('../reducer-path')
 const ReducerFunction = require('../reducer-function')
+const ReducerObject = require('../reducer-object')
 const ReducerEntity = require('../reducer-entity')
+const createTransform = require('../transform-expression/factory').create
 
 describe('reducer#create', () => {
   test('create path', () => {
-    const reducer = Factory.create('$foo.bar[2]')
+    const reducer = Factory.create(createTransform, '$foo.bar[2]')
     expect(reducer.type).toBe(ReducerPath.type)
     expect(reducer.name).toBe('foo.bar[2]')
   })
 
   test('create function', () => {
-    const reducer = Factory.create(() => true)
+    const reducer = Factory.create(createTransform, () => true)
     expect(reducer.type).toBe(ReducerFunction.type)
-    expect(reducer.body.length).toBe(0)
+    expect(reducer.body).toHaveLength(0)
+  })
+
+  test('create object', () => {
+    const reducer = Factory.create(ReducerObject, {})
+    expect(reducer.type).toBe(ReducerObject.type)
+    expect(reducer.props).toHaveLength(0)
   })
 
   test('create entity', () => {
-    const reducer = Factory.create('fooEntity:foo.bar')
+    const reducer = Factory.create(createTransform, 'fooEntity:foo.bar')
     expect(reducer.type).toBe(ReducerEntity.type)
     expect(reducer.name).toBe('foo.bar')
     expect(reducer.entityType).toBe('fooEntity')
   })
 
-  test(' detect invalid', () => {
+  test('detect invalid', () => {
     expect(() => {
-      Factory.create('a')
+      Factory.create(createTransform, 'a')
     }).toThrowErrorMatchingSnapshot()
   })
 })
diff --git a/packages/data-point/lib/transform-expression/__snapshots__/factory.test.js.snap b/packages/data-point/lib/transform-expression/__snapshots__/factory.test.js.snap
index 7a381cf..a9cef92 100644
--- a/packages/data-point/lib/transform-expression/__snapshots__/factory.test.js.snap
+++ b/packages/data-point/lib/transform-expression/__snapshots__/factory.test.js.snap
@@ -3,7 +3,7 @@
 exports[`validate It should throw error if transform is invalid 1`] = `
 "Could not parse a TransformExpression. The TransformExpression:
  true
-is not using a valid type, try using an Array, String or a Function.
+is not using a valid type, try using an Array, String, Object, or Function.
 More info: https://github.com/ViacomInc/data-point/tree/master/packages/data-point#transform-expression
 "
 `;
diff --git a/packages/data-point/lib/transform-expression/factory.test.js b/packages/data-point/lib/transform-expression/factory.test.js
index a0ecb2d..17aa38e 100644
--- a/packages/data-point/lib/transform-expression/factory.test.js
+++ b/packages/data-point/lib/transform-expression/factory.test.js
@@ -9,12 +9,12 @@ describe('isValid', () => {
     expect(TransformFactory.isValid('string')).toEqual(true)
     expect(TransformFactory.isValid(() => 1)).toEqual(true)
     expect(TransformFactory.isValid([])).toEqual(true)
+    expect(TransformFactory.isValid({})).toEqual(true)
   })
 
   test('should return false if invalid', () => {
     expect(TransformFactory.isValid(1)).toEqual(false)
     expect(TransformFactory.isValid(true)).toEqual(false)
-    expect(TransformFactory.isValid({})).toEqual(false)
   })
 })
 
diff --git a/packages/data-point/lib/transform-expression/resolve.test.js b/packages/data-point/lib/transform-expression/resolve.test.js
index fd27551..2d38887 100644
--- a/packages/data-point/lib/transform-expression/resolve.test.js
+++ b/packages/data-point/lib/transform-expression/resolve.test.js
@@ -47,7 +47,10 @@ test('resolve#reducer.resolveReducer', () => {
     value: testData.a.b.c
   })
 
-  const reducer = reducerFactory.create(reducers.addCollectionValues())
+  const reducer = reducerFactory.create(
+    TransformExpression.create,
+    reducers.addCollectionValues()
+  )
   return ResolveTransform.resolveReducer(store, accumulator, reducer).then(
     result => {
       expect(result.value).toEqual(6)