Repository: ViacomInc/data-point (js)

Issue:
Add support for constants in ReducerObjects
**Problem description**:

The values in `ReducerObjects` are always reducers, but that's inefficient if the reducers are just returning constants:

```js
{
  'transform:example': {
    // a, b, and c are constants wrapped in functions
    a: () => 'a',
    b: () => 'b',
    c: () => ({
      a: 1,
      b: 2
    })
  }
}
```

**Suggested solution**:

Add a `constant` reducer helper.

```js

const c = require('data-point').helpers.constant

{
  'transform:example': {
    a: c('a'),
    b: c('b'),
    c: c({
      a: 1,
      b: 2
    })
  }
}
```

Internally, the `ReducerObject` factory would combine these constants into a static object:

```js
{
  a: 'a',
  b: 'b',
  c: {
    a: 1,
    b: 2
  }
}
```

At run time, any reducers in the object are resolved, and their results are merged into the static object. This reduces any performance hit that might result from wrapping a bunch of constants inside of functions.




Pull request description:
feat(data-point/reducer-types): Add the ReducerConstant type
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**: Adds a `ReducerConstant` type

<!-- Why are these changes necessary? -->
**Why**: closes #170

<!-- How were these changes implemented? -->
**How**: `ReducerConstant` is a "reducer helper," so it's available via

```js
const c = require('data-point').helpers.constant
```

<!-- 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" -->
- [ ] Documentation
- [x] Tests
- [x] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->

Patch:
diff --git a/packages/data-point/README.md b/packages/data-point/README.md
index 9a6ee5b..e028106 100644
--- a/packages/data-point/README.md
+++ b/packages/data-point/README.md
@@ -36,6 +36,7 @@ npm install --save data-point
   - [map](#reducer-map)
   - [filter](#reducer-filter)
   - [find](#reducer-find)
+  - [constant](#reducer-constant)
 - [Entities](#entities)
   - [dataPoint.addEntities](#api-data-point-add-entities)
   - [Built-in entities](#built-in-entities)
@@ -283,6 +284,7 @@ This method returns a **Promise** with the final output value.
 
 - [Hello World](#hello-world) example.
 - [With options](#acc-locals-example) example.
+- [With constants in options](#options-with-constants) example.
 
 ### <a name="api-data-point-transform">dataPoint.transform()</a>
 
@@ -309,7 +311,7 @@ This method will return a **Promise** if `done` is omitted.
 |:---|:---|:---|
 | *reducer* | [Reducer](#reducers) | Reducer that manipulates the input. |
 | *input* | `*` | Input value that you want to transform. If **none**, pass `null` or empty object `{}`. |
-| *options* | [TransformOptions](#transform-options) | Options within the scope of the current transformation |
+| *options* | [Reducer](#reducers) | Request options. See this [example](#options-with-constants) for using constants in the reducer |
 | *done* | `function` _(optional)_ | Error-first callback [Node.js style callback](https://nodejs.org/api/errors.html#errors_node_js_style_callbacks) that has the arguments `(error, result)`, where `result` contains the final resolved [Accumulator](#accumulator). The actual transformation result will be inside the `result.value` property. |
 
 **<a name="transform-options">TransformOptions</a>**
@@ -638,7 +640,7 @@ Example at: [examples/reducer-function-error.js](examples/reducer-function-error
 
 ### <a name="object-reducer">ObjectReducer</a>
 
-ObjectReducers are plain objects where the values are reducers. They're used to aggregate data or transform objects.
+ObjectReducers are plain objects where the values are reducers. They're used to aggregate data or transform objects. For values that should be constants instead of reducers, you can use the [constant](#reducer-constant) reducer helper.
 
 <details>
   <summary>Transforming an object</summary>
@@ -1134,6 +1136,58 @@ find(reducer:Reducer):*
 
 Example at: [examples/reducer-helper-find.js](examples/reducer-helper-find.js)
 
+### <a name="reducer-constant">constant</a>
+
+The **constant** reducer always returns the given value.
+
+**SYNOPSIS**
+
+```js
+constant(value:*):*
+```
+
+**Reducer's arguments**
+
+| Argument | Type | Description |
+|:---|:---|:---|
+| *value* | * | The value the reducer should return |
+
+**EXAMPLE:**
+
+<details>
+  <summary>returning an object constant</summary>
+
+  ```js
+  const { constant } = DataPoint.helpers
+
+  const input = {
+    a: 1,
+    b: 2
+  }
+
+  const reducer = {
+    a: '$a',
+    b: constant({
+      a: '$a',
+      b: 3
+    })
+  }
+
+  dataPoint
+    .resolve(reducer, input) 
+    .then(output => {
+      // {
+      //   a: 1,
+      //   b: {
+      //     a: '$a',
+      //     b: 3
+      //   }
+      // }
+      }
+    })
+  ```
+</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. 
@@ -1703,7 +1757,7 @@ dataPoint.addEntities({
 | *inputType*  | String, [Reducer](#reducers) | type checks the entity's input value, does not mutate value. [Entity Type checking](#entity-type-check) |
 | *before*  | [Reducer](#reducers) | reducer to be resolved **before** the entity resolution |
 | *url*   | [StringTemplate](#string-template) | String value to resolve the request's url |
-| *options* | [Reducer](#reducers) | reducer that should return an object to use as request options. These map directly to [request.js](https://github.com/request/request) options
+| *options* | [Reducer](#reducers) | reducer that returns an object to use as [request.js](https://github.com/request/request) options
 | *after*   | [Reducer](#reducers) | reducer to be resolved **after** the entity resolution |
 | *error*   | [Reducer](#reducers) | reducer to be resolved in case of an error |
 | *outputType*  | String, [Reducer](#reducers) | type checks the entity's output value, does not mutate value. [Entity Type checking](#entity-type-check) |
@@ -1744,7 +1798,7 @@ Example at: [examples/entity-request-basic.js](examples/entity-request-basic.js)
 
 StringTemplate is a string that supports a **minimal** templating system. You may inject any value into the string by enclosing it within `{ObjectPath}` curly braces. **The context of the string is the Request's [Accumulator](#accumulator) Object**, meaning you have access to any property within it. 
 
-Using `acc.value` property to make the url dynamic.
+Using `acc.value` property to make the url dynamic:
 
 <details>
   <summary>`acc.value` Example</summary>
@@ -1775,11 +1829,10 @@ Using `acc.value` property to make the url dynamic.
   ```
 </details>
 
+Example at: [examples/entity-request-string-template.js](examples/entity-request-string-template.js)
 
 <a name="acc-locals-example" >Using `acc.locals` property to make the url dynamic:</a>
 
-For more information on acc.locals: [TransformOptions](#transform-options) and [Accumulator](#accumulator) Objects.
-
 <details>
   <summary>`acc.locals` example</summary>
   
@@ -1811,8 +1864,52 @@ For more information on acc.locals: [TransformOptions](#transform-options) and [
   ```
 </details>
 
+Example at: [examples/entity-request-options-locals.js](examples/entity-request-options-locals.js)
+
+For more information on acc.locals: [TransformOptions](#transform-options) and [Accumulator](#accumulator) Objects.
+
+<a name="options-with-constants" >Using constants in the options reducer:</a>
+
+<details>
+  <summary>constants example</summary>
+
+  ```js
+  const DataPoint = require('data-point')
+  const c = DataPoint.helpers.constant
+  const dataPoint = DataPoint.create()
+
+  dataPoint.addEntities({
+    'request:searchPeople': {
+      url: 'https://swapi.co/api/people',
+      // options is a Reducer, but values
+      // at any level can be wrapped as
+      // constants (or just wrap the whole
+      // object if all the values are static)
+      options: {
+        'content-type': c('application/json'), // constant
+        qs: {
+          // get path `searchTerm` from input
+          // to dataPoint.resolve
+          search: '$searchTerm'
+        }
+      }
+    }
+  })
+
+  const input = {
+    searchTerm: 'r2'
+  }
+
+  // the second parameter to transform is the input value
+  dataPoint
+    .resolve('request:searchPeople', input)
+    .then(output => {
+      assert.equal(output.results[0].name, 'R2-D2')
+    })
+  ```
+</details>
 
-Example at: [examples/entity-request-string-template.js](examples/entity-request-options-locals.js)
+Example at: [examples/entity-request-options.js](examples/entity-request-options.js)
 
 For more examples of request entities, see the [Examples](examples), the [Integration Examples](test/definitions/integrations.js), and the unit tests: [Request Definitions](test/definitions/sources.js).
 
diff --git a/packages/data-point/examples/entity-request-options.js b/packages/data-point/examples/entity-request-options.js
new file mode 100644
index 0000000..f1a0fb6
--- /dev/null
+++ b/packages/data-point/examples/entity-request-options.js
@@ -0,0 +1,38 @@
+const assert = require('assert')
+const mock = require('./entity-request-options.mock')
+
+const DataPoint = require('..')
+
+const c = DataPoint.helpers.constant
+const dataPoint = DataPoint.create()
+
+dataPoint.addEntities({
+  'request:searchPeople': {
+    url: 'https://swapi.co/api/people',
+    // options is a "ReducerObject", but
+    // values at any level can be wrapped
+    // as constants (or just wrap the whole
+    // object if all the values are static)
+    options: {
+      'content-type': c('application/json'), // constant
+      qs: {
+        // get path `searchTerm` from input
+        // to dataPoint.resolve
+        search: '$searchTerm'
+      }
+    }
+  }
+})
+
+// this will mock the remote service
+mock()
+
+const input = {
+  searchTerm: 'r2'
+}
+
+// the second parameter to transform is the input value
+dataPoint.resolve('request:searchPeople', input).then(output => {
+  assert.equal(output.results[0].name, 'R2-D2')
+  console.dir(output, { colors: true })
+})
diff --git a/packages/data-point/examples/entity-request-options.mock.js b/packages/data-point/examples/entity-request-options.mock.js
new file mode 100644
index 0000000..f39da3c
--- /dev/null
+++ b/packages/data-point/examples/entity-request-options.mock.js
@@ -0,0 +1,19 @@
+const nock = require('nock')
+
+module.exports = () => {
+  nock('https://swapi.co')
+    .get('/api/people')
+    .query({
+      search: 'r2'
+    })
+    .reply(200, {
+      count: 1,
+      next: null,
+      previous: null,
+      results: [
+        {
+          name: 'R2-D2'
+        }
+      ]
+    })
+}
diff --git a/packages/data-point/lib/entity-types/entity-collection/resolve.js b/packages/data-point/lib/entity-types/entity-collection/resolve.js
index 158c31d..39f46a3 100644
--- a/packages/data-point/lib/entity-types/entity-collection/resolve.js
+++ b/packages/data-point/lib/entity-types/entity-collection/resolve.js
@@ -23,18 +23,18 @@ function validateAsArray (acc) {
   return acc.value instanceof Array
     ? acc
     : Promise.reject(
-      new Error(
-        Util.format(
-          '%s received value = %s of type %s,',
-          entity.id,
-          _.truncate(Util.inspect(acc.value, { breakLength: Infinity }), {
-            length: 30
-          }),
-          utils.typeOf(acc.value),
-          'this entity only resolves Array values. More info https://github.com/ViacomInc/data-point/tree/master/packages/data-point#collection-entity'
+        new Error(
+          Util.format(
+            '%s received value = %s of type %s,',
+            entity.id,
+            _.truncate(Util.inspect(acc.value, { breakLength: Infinity }), {
+              length: 30
+            }),
+            utils.typeOf(acc.value),
+            'this entity only resolves Array values. More info https://github.com/ViacomInc/data-point/tree/master/packages/data-point#collection-entity'
+          )
         )
       )
-    )
 }
 
 /**
diff --git a/packages/data-point/lib/helpers/helpers.js b/packages/data-point/lib/helpers/helpers.js
index 4e1d74a..2fe8424 100644
--- a/packages/data-point/lib/helpers/helpers.js
+++ b/packages/data-point/lib/helpers/helpers.js
@@ -8,6 +8,7 @@ const typeCheckFunctionReducers = require('./type-check-function-reducers')
 
 module.exports.helpers = {
   assign: stubFactories.assign,
+  constant: stubFactories.constant,
   filter: stubFactories.filter,
   find: stubFactories.find,
   map: stubFactories.map,
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 2b0076f..0bce23c 100644
--- a/packages/data-point/lib/reducer-types/reducer-helpers/index.js
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/index.js
@@ -1,6 +1,7 @@
 const createStub = require('./reducer-stub').create
 
 const reducerAssign = require('./reducer-assign')
+const reducerConstant = require('./reducer-constant')
 const reducerFilter = require('./reducer-filter')
 const reducerFind = require('./reducer-find')
 const reducerMap = require('./reducer-map')
@@ -30,6 +31,7 @@ module.exports.isType = require('./reducer-stub').isType
 
 const reducers = {
   [reducerAssign.type]: reducerAssign,
+  [reducerConstant.type]: reducerConstant,
   [reducerFilter.type]: reducerFilter,
   [reducerFind.type]: reducerFind,
   [reducerMap.type]: reducerMap,
@@ -45,6 +47,7 @@ function bindStubFunction (reducerType) {
 
 const stubFactories = {
   [reducerAssign.name]: bindStubFunction(reducerAssign.type),
+  [reducerConstant.name]: bindStubFunction(reducerConstant.type),
   [reducerFilter.name]: bindStubFunction(reducerFilter.type),
   [reducerFind.name]: bindStubFunction(reducerFind.type),
   [reducerMap.name]: bindStubFunction(reducerMap.type),
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.js
new file mode 100644
index 0000000..88afdc1
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.js
@@ -0,0 +1,33 @@
+const REDUCER_CONSTANT = 'ReducerConstant'
+
+module.exports.type = REDUCER_CONSTANT
+
+const HELPER_NAME = 'constant'
+
+module.exports.name = HELPER_NAME
+
+/**
+ * @class
+ * @property {string} type
+ * @property {*} value
+ */
+function ReducerConstant () {
+  this.type = REDUCER_CONSTANT
+  this.value = undefined
+}
+
+module.exports.ReducerConstant = ReducerConstant
+
+/**
+ * @param {Function} createReducer
+ * @param {*} value
+ * @return {ReducerConstant}
+ */
+function create (createReducer, value) {
+  const reducer = new ReducerConstant()
+  reducer.value = value
+
+  return reducer
+}
+
+module.exports.create = create
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/index.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/index.js
new file mode 100644
index 0000000..fafee9c
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/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-constant/resolve.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/resolve.js
new file mode 100644
index 0000000..1067c5e
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/resolve.js
@@ -0,0 +1,17 @@
+const Promise = require('bluebird')
+
+const utils = require('../../../utils')
+
+/**
+ * @param {Object} manager
+ * @param {Function} resolveReducer
+ * @param {Accumulator} accumulator
+ * @param {ReducerConstant} reducerConstant
+ * @returns {Promise<Accumulator>}
+ */
+function resolve (manager, resolveReducer, accumulator, reducerConstant) {
+  const acc = utils.set(accumulator, 'value', reducerConstant.value)
+  return Promise.resolve(acc)
+}
+
+module.exports.resolve = resolve
diff --git a/packages/data-point/lib/reducer-types/reducer-list/factory.js b/packages/data-point/lib/reducer-types/reducer-list/factory.js
index b210146..480b27a 100644
--- a/packages/data-point/lib/reducer-types/reducer-list/factory.js
+++ b/packages/data-point/lib/reducer-types/reducer-list/factory.js
@@ -8,9 +8,11 @@ module.exports.type = REDUCER_LIST
  * @class
  * @property {string} type
  * @property {Array<reducer>} reducers
+ * @property {boolean} isEmpty
  */
 function ReducerList () {
   this.type = 'ReducerList'
+  this.isEmpty = undefined
   this.reducers = []
 }
 
@@ -79,10 +81,10 @@ module.exports.parse = parse
  */
 function create (createReducer, source = []) {
   const tokens = parse(source)
-
   const reducers = tokens.map(token => createReducer(token))
 
   const reducer = new ReducerList()
+  reducer.isEmpty = _.isEmpty(reducers)
   reducer.reducers = reducers
 
   return reducer
diff --git a/packages/data-point/lib/reducer-types/reducer-object/factory.js b/packages/data-point/lib/reducer-types/reducer-object/factory.js
index 5267071..7c95714 100644
--- a/packages/data-point/lib/reducer-types/reducer-object/factory.js
+++ b/packages/data-point/lib/reducer-types/reducer-object/factory.js
@@ -1,4 +1,4 @@
-const isPlainObject = require('lodash/isPlainObject')
+const _ = require('lodash')
 
 const REDUCER_OBJECT = 'ReducerObject'
 
@@ -7,11 +7,15 @@ module.exports.type = REDUCER_OBJECT
 /**
  * @class
  * @property {string} type - @see reducerType
- * @property {Array} props
+ * @property {Function} source
+ * @property {Array<Object>} reducers
+ * @property {boolean} isEmpty
  */
 function ReducerObject () {
   this.type = REDUCER_OBJECT
-  this.props = undefined
+  this.isEmpty = undefined
+  this.source = undefined
+  this.reducers = undefined
 }
 
 module.exports.ReducerObject = ReducerObject
@@ -21,38 +25,64 @@ module.exports.ReducerObject = ReducerObject
  * @returns {boolean}
  */
 function isType (source) {
-  return isPlainObject(source)
+  return _.isPlainObject(source)
 }
 
 module.exports.isType = isType
 
+/**
+ * @return {Object}
+ */
+function newProps () {
+  return {
+    constants: {},
+    reducers: []
+  }
+}
+
+module.exports.newProps = newProps
+
 /**
  * @param {Function} createReducer
  * @param {Object} source
- * @param {Array} path
- * @param {Array} props
+ * @param {Array} stack
+ * @param {Object} props
  * @returns {Array}
  */
-function getReducerProps (createReducer, source, path = [], props = []) {
+function getProps (createReducer, source, stack = [], props = newProps()) {
   for (let key of Object.keys(source)) {
+    const path = stack.concat(key)
     const value = source[key]
-    const fullPath = path.concat(key)
-    if (isPlainObject(value)) {
+    if (_.isPlainObject(value)) {
       // NOTE: recursive call
-      getReducerProps(createReducer, value, fullPath, props)
+      getProps(createReducer, value, path, props)
       continue
     }
 
-    props.push({
-      path: fullPath,
-      reducer: createReducer(value)
-    })
+    const reducer = createReducer(value)
+    if (reducer.type === 'ReducerConstant') {
+      _.set(props.constants, path, reducer.value)
+    } else {
+      props.reducers.push({ path, reducer })
+    }
   }
 
   return props
 }
 
-module.exports.getReducerProps = getReducerProps
+module.exports.getProps = getProps
+
+/**
+ * @param {Object} source
+ * @return {Function}
+ */
+function getSourceFunction (source) {
+  const fn = () => _.cloneDeep(source)
+  Object.defineProperty(fn, 'name', { value: 'source' })
+  return fn
+}
+
+module.exports.getSourceFunction = getSourceFunction
 
 /**
  * @param {Function} createReducer
@@ -60,8 +90,12 @@ module.exports.getReducerProps = getReducerProps
  * @returns {reducer}
  */
 function create (createReducer, source = {}) {
+  const props = getProps(createReducer, source)
+
   const reducer = new ReducerObject()
-  reducer.props = getReducerProps(createReducer, source)
+  reducer.isEmpty = _.isEmpty(props.constants) && _.isEmpty(props.reducers)
+  reducer.source = getSourceFunction(props.constants)
+  reducer.reducers = props.reducers
 
   return reducer
 }
diff --git a/packages/data-point/lib/reducer-types/reducer-object/resolve.js b/packages/data-point/lib/reducer-types/reducer-object/resolve.js
index 2f4cdae..30f3b9b 100644
--- a/packages/data-point/lib/reducer-types/reducer-object/resolve.js
+++ b/packages/data-point/lib/reducer-types/reducer-object/resolve.js
@@ -1,5 +1,6 @@
 const Promise = require('bluebird')
 const set = require('lodash/set')
+
 const utils = require('../../utils')
 
 /**
@@ -10,11 +11,11 @@ const utils = require('../../utils')
  * @returns {Promise<Accumulator>}
  */
 function resolve (manager, resolveReducer, accumulator, reducer) {
-  if (reducer.props.length === 0) {
+  if (utils.reducerIsEmpty(reducer)) {
     return Promise.resolve(accumulator)
   }
 
-  return Promise.map(reducer.props, ({ path, reducer }) => {
+  return Promise.map(reducer.reducers, ({ reducer, path }) => {
     return resolveReducer(manager, accumulator, reducer).then(({ value }) => ({
       path,
       value
@@ -22,8 +23,9 @@ function resolve (manager, resolveReducer, accumulator, reducer) {
   }).then(result => {
     const value = result.reduce(
       (acc, { path, value }) => set(acc, path, value),
-      {}
+      reducer.source()
     )
+
     return utils.set(accumulator, 'value', value)
   })
 }
diff --git a/packages/data-point/lib/utils/index.js b/packages/data-point/lib/utils/index.js
index a4c5ec9..3d610bb 100644
--- a/packages/data-point/lib/utils/index.js
+++ b/packages/data-point/lib/utils/index.js
@@ -92,19 +92,7 @@ module.exports.inspectProperties = inspectProperties
  * @returns {boolean}
  */
 function reducerIsEmpty (reducer) {
-  if (!reducer) {
-    return true
-  }
-
-  if (reducer.type === 'ReducerList') {
-    return _.isEmpty(reducer.reducers)
-  }
-
-  if (reducer.type === 'ReducerObject') {
-    return _.isEmpty(reducer.props)
-  }
-
-  return false
+  return !reducer || !!reducer.isEmpty
 }
 
 module.exports.reducerIsEmpty = reducerIsEmpty


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 bff5c45..572af7e 100644
--- a/packages/data-point/lib/__snapshots__/index.test.js.snap
+++ b/packages/data-point/lib/__snapshots__/index.test.js.snap
@@ -7,6 +7,7 @@ Object {
   "createReducerResolver": [Function],
   "helpers": Object {
     "assign": [Function],
+    "constant": [Function],
     "filter": [Function],
     "find": [Function],
     "isArray": [Function],
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 1f0ff78..984ea36 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
@@ -140,8 +140,9 @@ describe('factory#parse composed modifiers', () => {
     )
 
     expect(result.compose.reducers[1]).toHaveProperty('type', 'ReducerObject')
-    expect(result.compose.reducers[1].props).toHaveLength(1)
-    expect(result.compose.reducers[1].props[0].reducer).toHaveProperty(
+    expect(result.compose.reducers[1].source()).toEqual({})
+    expect(result.compose.reducers[1].reducers).toHaveLength(1)
+    expect(result.compose.reducers[1].reducers[0].reducer).toHaveProperty(
       'type',
       'ReducerPath'
     )
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 149d4f4..2ec9bee 100644
--- a/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap
+++ b/packages/data-point/lib/helpers/__snapshots__/helpers.test.js.snap
@@ -8,6 +8,7 @@ Object {
   "createReducerResolver": [Function],
   "helpers": Object {
     "assign": [Function],
+    "constant": [Function],
     "filter": [Function],
     "find": [Function],
     "isArray": [Function],
diff --git a/packages/data-point/lib/reducer-types/factory.test.js b/packages/data-point/lib/reducer-types/factory.test.js
index c1061df..bf4f496 100644
--- a/packages/data-point/lib/reducer-types/factory.test.js
+++ b/packages/data-point/lib/reducer-types/factory.test.js
@@ -24,7 +24,8 @@ describe('reducer#create', () => {
   test('create object', () => {
     const reducer = Factory.create({})
     expect(reducer.type).toBe(ReducerObject.type)
-    expect(reducer.props).toHaveLength(0)
+    expect(reducer.source()).toEqual({})
+    expect(reducer.reducers).toHaveLength(0)
   })
 
   test('create entity', () => {
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.test.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.test.js
new file mode 100644
index 0000000..ede8daa
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/factory.test.js
@@ -0,0 +1,23 @@
+/* eslint-env jest */
+
+const Factory = require('./factory')
+const createReducer = require('../../index').create
+
+describe('ReducerConstant#factory', () => {
+  test('reducer with undefined as value', () => {
+    const reducer = Factory.create(createReducer)
+    expect(reducer).toBeInstanceOf(Factory.ReducerConstant)
+    expect(reducer.type).toBe(Factory.type)
+    expect(reducer.value).toBe(undefined)
+  })
+  test('reducers with different value types', () => {
+    expect(Factory.create(createReducer, null).value).toBe(null)
+    expect(Factory.create(createReducer, 100).value).toBe(100)
+    expect(Factory.create(createReducer, 'string').value).toBe('string')
+    expect(Factory.create(createReducer, ['a', 'b']).value).toEqual(['a', 'b'])
+    expect(Factory.create(createReducer, { a: 1, b: 2 }).value).toEqual({
+      a: 1,
+      b: 2
+    })
+  })
+})
diff --git a/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/resolve.test.js b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/resolve.test.js
new file mode 100644
index 0000000..ebf72d0
--- /dev/null
+++ b/packages/data-point/lib/reducer-types/reducer-helpers/reducer-constant/resolve.test.js
@@ -0,0 +1,68 @@
+/* eslint-env jest */
+
+const DataPoint = require('../../../index')
+const constant = DataPoint.helpers.constant
+
+const dataPoint = DataPoint.create()
+
+describe('ReducerConstant#resolve', () => {
+  test('constant with nested object', () => {
+    const source = {
+      a: '$a',
+      b: 1,
+      c: {
+        a: '$c.a'
+      }
+    }
+    const input = {}
+
+    return dataPoint
+      .transform(constant(source), input)
+      .then(result => expect(result.value).toEqual(source))
+  })
+
+  test('constant with a function', () => {
+    const source = acc => {
+      throw new Error()
+    }
+    const input = 1
+
+    return dataPoint
+      .transform(constant(source), input)
+      .catch(e => e)
+      .then(result => {
+        expect(result).not.toBeInstanceOf(Error)
+        expect(result.value).toEqual(source)
+        expect(() => result.value()).toThrow()
+      })
+  })
+
+  test('constants inside of a ReducerObject', () => {
+    const source = {
+      a: '$a',
+      b: '$b',
+      c: constant({
+        a: '$c.a'
+      }),
+      d: constant(1)
+    }
+    const input = {
+      a: 1,
+      b: 2,
+      c: {
+        a: 1
+      }
+    }
+
+    return dataPoint.transform(source, input).then(result =>
+      expect(result.value).toEqual({
+        a: 1,
+        b: 2,
+        c: {
+          a: '$c.a'
+        },
+        d: 1
+      })
+    )
+  })
+})
diff --git a/packages/data-point/lib/reducer-types/reducer-object/__snapshots__/factory.test.js.snap b/packages/data-point/lib/reducer-types/reducer-object/__snapshots__/factory.test.js.snap
index 5cf1525..8773698 100644
--- a/packages/data-point/lib/reducer-types/reducer-object/__snapshots__/factory.test.js.snap
+++ b/packages/data-point/lib/reducer-types/reducer-object/__snapshots__/factory.test.js.snap
@@ -1,80 +1,138 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`reducer/reducer-function#getReducerProps should accept a non-empty object 1`] = `
-Array [
-  Object {
-    "path": Array [
-      "a1",
-    ],
-    "reducer": ReducerPath {
-      "asCollection": true,
-      "body": [Function],
-      "name": "a",
-      "type": "ReducerPath",
-      Symbol(reducer): true,
+exports[`ReducerObject.factory#create reducer object with no props argument 1`] = `
+ReducerObject {
+  "isEmpty": true,
+  "reducers": Array [],
+  "source": [Function],
+  "type": "ReducerObject",
+}
+`;
+
+exports[`ReducerObject.factory#create reducer object with props argument 1`] = `
+ReducerObject {
+  "isEmpty": false,
+  "reducers": Array [
+    Object {
+      "path": Array [
+        "a",
+      ],
+      "reducer": ReducerPath {
+        "asCollection": false,
+        "body": [Function],
+        "name": "a",
+        "type": "ReducerPath",
+        Symbol(reducer): true,
+      },
     },
-  },
-  Object {
-    "path": Array [
-      "b1",
-      "a2",
-    ],
-    "reducer": ReducerList {
-      "reducers": Array [
-        ReducerPath {
-          "asCollection": false,
-          "body": [Function],
-          "name": "a2",
-          "type": "ReducerPath",
-          Symbol(reducer): true,
-        },
-        ReducerFunction {
-          "body": [Function],
-          "type": "ReducerFunction",
-          Symbol(reducer): true,
-        },
+    Object {
+      "path": Array [
+        "b",
       ],
-      "type": "ReducerList",
-      Symbol(reducer): true,
+      "reducer": ReducerPath {
+        "asCollection": false,
+        "body": [Function],
+        "name": "b",
+        "type": "ReducerPath",
+        Symbol(reducer): true,
+      },
+    },
+  ],
+  "source": [Function],
+  "type": "ReducerObject",
+}
+`;
+
+exports[`ReducerObject.factory#getProps should accept a non-empty object 1`] = `
+Object {
+  "constants": Object {
+    "b1": Object {
+      "c2": 1,
+    },
+    "c1": Object {
+      "a": 1,
+      "b": 2,
     },
+    "d1": 5,
   },
-  Object {
-    "path": Array [
-      "b1",
-      "b2",
-    ],
-    "reducer": ReducerObject {
-      "props": Array [
-        Object {
-          "path": Array [
-            "a3",
-          ],
-          "reducer": ReducerEntity {
+  "reducers": Array [
+    Object {
+      "path": Array [
+        "a1",
+      ],
+      "reducer": ReducerPath {
+        "asCollection": true,
+        "body": [Function],
+        "name": "a",
+        "type": "ReducerPath",
+        Symbol(reducer): true,
+      },
+    },
+    Object {
+      "path": Array [
+        "b1",
+        "a2",
+      ],
+      "reducer": ReducerList {
+        "isEmpty": false,
+        "reducers": Array [
+          ReducerPath {
             "asCollection": false,
-            "entityType": "transform",
-            "hasEmptyConditional": false,
-            "id": "transform:entity-name",
-            "name": "entity-name",
-            "type": "ReducerEntity",
+            "body": [Function],
+            "name": "a2",
+            "type": "ReducerPath",
             Symbol(reducer): true,
           },
-        },
-        Object {
-          "path": Array [
-            "b3",
-          ],
-          "reducer": ReducerPath {
-            "asCollection": false,
+          ReducerFunction {
             "body": [Function],
-            "name": "b1.b2.b3",
-            "type": "ReducerPath",
+            "type": "ReducerFunction",
             Symbol(reducer): true,
           },
-        },
+        ],
+        "type": "ReducerList",
+        Symbol(reducer): true,
+      },
+    },
+    Object {
+      "path": Array [
+        "b1",
+        "b2",
       ],
-      "type": "ReducerObject",
-      Symbol(reducer): true,
+      "reducer": ReducerObject {
+        "isEmpty": false,
+        "reducers": Array [
+          Object {
+            "path": Array [
+              "a3",
+            ],
+            "reducer": ReducerEntity {
+              "asCollection": false,
+              "entityType": "transform",
+              "hasEmptyConditional": false,
+              "id": "transform:entity-name",
+              "name": "entity-name",
+              "type": "ReducerEntity",
+              Symbol(reducer): true,
+            },
+          },
+          Object {
+            "path": Array [
+              "b3",
+            ],
+            "reducer": ReducerPath {
+              "asCollection": false,
+              "body": [Function],
+              "name": "b1.b2.b3",
+              "type": "ReducerPath",
+              Symbol(reducer): true,
+            },
+          },
+        ],
+        "source": [Function],
+        "type": "ReducerObject",
+        Symbol(reducer): true,
+      },
     },
-  },
-]
+  ],
+}
 `;
diff --git a/packages/data-point/lib/reducer-types/reducer-object/factory.test.js b/packages/data-point/lib/reducer-types/reducer-object/factory.test.js
index 6613921..d68f52b 100644
--- a/packages/data-point/lib/reducer-types/reducer-object/factory.test.js
+++ b/packages/data-point/lib/reducer-types/reducer-object/factory.test.js
@@ -2,55 +2,79 @@
 
 const factory = require('./factory')
 const createReducer = require('../index').create
+const constant = require('../..').helpers.constant
 
-test('reducer/reducer-function#isType', () => {
+test('ReducerObject.factory#isType', () => {
   expect(factory.isType('$foo')).toBe(false)
   expect(factory.isType(() => true)).toBe(false)
   expect(factory.isType([])).toBe(false)
   expect(factory.isType({})).toBe(true)
 })
 
-describe('reducer/reducer-function#getReducerProps', () => {
+describe('ReducerObject.factory#getProps', () => {
   it('should accept an empty object', () => {
-    const reducer = factory.getReducerProps(createReducer, {})
-    expect(reducer).toEqual([])
+    const reducer = factory.getProps(createReducer, {})
+    expect(reducer).toEqual({
+      constants: {},
+      reducers: []
+    })
   })
   it('should accept a non-empty object', () => {
-    const reducer = factory.getReducerProps(createReducer, {
+    const props = factory.getProps(createReducer, {
       a1: '$a[]',
       b1: {
         a2: ['$a2', () => false],
         b2: [
           {
             a3: 'transform:entity-name',
-            b3: '$b1.b2.b3'
+            b3: '$b1.b2.b3',
+            c3: constant('CONSTANT')
           }
-        ]
-      }
+        ],
+        c2: constant(1)
+      },
+      c1: constant({
+        a: 1,
+        b: 2
+      }),
+      d1: constant(5)
     })
-    expect(reducer).toMatchSnapshot()
+    expect(props).toMatchSnapshot()
   })
 })
 
-describe('reducer/reducer-path#create', () => {
+describe('ReducerObject.factory#getSourceFunction', () => {
+  it('creates a function that returns a new object', () => {
+    const input = {
+      a: 1,
+      b: {
+        c: '1',
+        d: () => true
+      }
+    }
+    const fn = factory.getSourceFunction(input)
+    const output = fn()
+
+    expect(fn.name).toBe('source')
+    expect(input === output).toBe(false)
+    expect(input).toEqual(output)
+    expect(output.b.d()).toBe(true)
+  })
+})
+
+describe('ReducerObject.factory#create', () => {
   it('reducer object with no props argument', () => {
     const reducer = factory.create(createReducer)
-    expect(reducer.type).toBe('ReducerObject')
-    expect(reducer.props).toBeInstanceOf(Array)
-  })
-  it('reducer object with empty props argument', () => {
-    const reducer = factory.create(createReducer, {})
-    expect(reducer.type).toBe('ReducerObject')
-    expect(reducer.props).toBeInstanceOf(Array)
+    expect(reducer).toMatchSnapshot()
+    expect(reducer.source()).toEqual({})
   })
   it('reducer object with props argument', () => {
     const reducer = factory.create(createReducer, {
       a: '$a',
       b: '$b',
-      c: '$c'
+      c: constant('c')
     })
-    expect(reducer.type).toBe('ReducerObject')
-    expect(reducer.props).toBeInstanceOf(Array)
-    expect(reducer.props).toHaveLength(3)
+    expect(reducer).toMatchSnapshot()
+    expect(reducer.source()).toEqual({ c: 'c' })
   })
 })
diff --git a/packages/data-point/lib/reducer-types/reducer-object/resolve.test.js b/packages/data-point/lib/reducer-types/reducer-object/resolve.test.js
index bda4b35..9c976f8 100644
--- a/packages/data-point/lib/reducer-types/reducer-object/resolve.test.js
+++ b/packages/data-point/lib/reducer-types/reducer-object/resolve.test.js
@@ -6,6 +6,7 @@ const createReducer = require('../index').create
 const resolveReducer = require('../index').resolve
 const AccumulatorFactory = require('../../accumulator/factory')
 const FixtureStore = require('../../../test/utils/fixture-store')
+const constant = require('../..').helpers.constant
 
 let dataPoint
 
@@ -37,7 +38,21 @@ describe('resolve#reducerObject.resolve', () => {
     })
   })
 
-  it('should resolve a reducer object for reducer that changes value', () => {
+  it('should resolve a nested reducer object #1', () => {
+    const reducer = createReducerObject(createReducer, {
+      x: constant([1, 2]),
+      y: {
+        a: constant({
+          p1: 1,
+          p2: {
+            p3: 2
+          }
+        }),
+        b: '$x.y'
+      },
+      zPlusOne: ['$x.y.z', input => input + 1]
+    })
+
     const accumulator = AccumulatorFactory.create({
       value: {
         x: {
@@ -48,11 +63,6 @@ describe('resolve#reducerObject.resolve', () => {
       }
     })
 
-    const reducer = createReducerObject(createReducer, {
-      y: '$x.y',
-      zPlusOne: ['$x.y.z', input => input + 1]
-    })
-
     return resolveReducerObject(
       dataPoint,
       resolveReducer,
@@ -60,17 +70,41 @@ describe('resolve#reducerObject.resolve', () => {
       reducer
     ).then(result => {
       expect(result.value).toEqual({
+        x: [1, 2],
         y: {
-          z: 2
+          a: {
+            p1: 1,
+            p2: {
+              p3: 2
+            }
+          },
+          b: {
+            z: 2
+          }
         },
         zPlusOne: 3
       })
     })
   })
 
-  it('should resolve a reducer object that has reducer object inside reducer', () => {
+  it('should resolve a nested reducer object #2', () => {
+    const reducer = createReducerObject(createReducer, {
+      x: '$c.x',
+      y: '$c.y',
+      z: {
+        a: '$a',
+        b: '$b',
+        c: constant({
+          x: '$c.x',
+          y: {
+            y2: '$c.y'
+          },
+          z: ['$c.z']
+        })
+      }
+    })
+
     const accumulator = AccumulatorFactory.create({
-      // this is the input
       value: {
         a: 'A',
         b: 'B',
@@ -81,16 +115,6 @@ describe('resolve#reducerObject.resolve', () => {
       }
     })
 
-    const reducer = createReducerObject(createReducer, {
-      // this is the function itself
-      x: '$c.x',
-      y: '$c.y',
-      z: {
-        a: '$a',
-        b: '$b'
-      }
-    })
-
     return resolveReducerObject(
       dataPoint,
       resolveReducer,
@@ -102,13 +126,19 @@ describe('resolve#reducerObject.resolve', () => {
         y: 'Y',
         z: {
           a: 'A',
-          b: 'B'
+          b: 'B',
+          c: {
+            x: '$c.x',
+            y: {
+              y2: '$c.y'
+            },
+            z: ['$c.z']
+          }
         }
       })
     })
   })
-
-  it('it should resolve a reducer object whose value follows input schematics', () => {
+  it('should move object values to different levels of nesting', () => {
     const accumulator = AccumulatorFactory.create({
       value: {
         a: {