Repository: ViacomInc/data-point
Pull request #100 (2018-01-02 18:31:10)

feat(data-point/entities): adds conditional reducer operator
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**: adds conditional operator #46

<!-- Why are these changes necessary? -->
**Why**: allows entities to be conditionally executed

<!-- 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? -->
- [N/A] Added username to **all-contributors** list

<!-- feel free to add additional comments -->


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

Coverage remained the same at 100.0% when pulling **9236b2da21f88d6854148385e74d41d344127edd on reducer-conditional-operator** into **9a884ddb354272e27062797e046ed91d4c7c5afa on master**.

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

Coverage remained the same at 100.0% when pulling **9236b2da21f88d6854148385e74d41d344127edd on reducer-conditional-operator** into **9a884ddb354272e27062797e046ed91d4c7c5afa on master**.

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

Coverage remained the same at 100.0% when pulling **ae5caf194804a6594caa90f4822c0682e7ccb665 on reducer-conditional-operator** into **9a884ddb354272e27062797e046ed91d4c7c5afa on master**.

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

Coverage remained the same at 100.0% when pulling **ae5caf194804a6594caa90f4822c0682e7ccb665 on reducer-conditional-operator** into **9a884ddb354272e27062797e046ed91d4c7c5afa on master**.

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

Coverage remained the same at 100.0% when pulling **6a3851593d0f5f129f61855a369ff09c74f99f8d on reducer-conditional-operator** into **b5d848a88e21d21352f1a2a56540d0fcbbc82ccb on master**.

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

Coverage remained the same at 100.0% when pulling **6a3851593d0f5f129f61855a369ff09c74f99f8d on reducer-conditional-operator** into **b5d848a88e21d21352f1a2a56540d0fcbbc82ccb on master**.


Patch:
diff --git a/packages/data-point/README.md b/packages/data-point/README.md
index 93e26c8..e5850a9 100644
--- a/packages/data-point/README.md
+++ b/packages/data-point/README.md
@@ -785,13 +785,14 @@ For information about supported (built-in) entities, see the [Entities](#entitie
 **SYNOPSIS**
 
 ```js
-'<EntityType>:<EntityId>[[]]'
+'[?]<EntityType>:<EntityId>[[]]'
 ```
 
 **OPTIONS**
 
 | Option | Type | Description |
 |:---|:---|:---|
+| *?* | `String` | Only execute entity if `acc.value` is not equal to `false`, `null` or `undefined`. |
 | *EntityType* | `String` | Valid Entity type. |
 | *EntityID* | `String` | Valid Entity ID. Optionally, you may pass Closed brackets at the end `[]` to indicate collection mapping. |
 
@@ -825,6 +826,58 @@ For information about supported (built-in) entities, see the [Entities](#entitie
 </details>
 
 
+### <a name="reducer-conditional-operator">Conditionally execute an entity</a>
+
+Only execute an entity if the accumulator value is **not** equal to `false`, `null` or `undefined`. If the conditional is not met, the entity will not be executed and the value will remain the same.
+
+<details>
+  <summary>Conditionally execute an entity</summary>
+
+```js
+const people = [
+  {
+    name: 'Luke Skywalker',
+    swapiId: '1'
+  },
+  {
+    name: 'Yoda',
+    swapiId: null
+  }
+]
+
+dataPoint.addEntities({
+  'request:getPerson': {
+    url: 'https://swapi.co/api/people/{value}'
+  },
+  'transform:getPerson': {
+    name: '$name',
+    // request:getPerson will only
+    // be executed if swapiId is
+    // not false, null or undefined
+    birthYear: '$swapiId | ?request:getPerson | $birth_year'
+  }
+})
+
+dataPoint
+  .transform('transform:getPerson[]', people)
+  .then((acc) => {
+    assert.deepEqual(acc.value, [
+      {
+        name: 'Luke Skywalker',
+        birthYear: '19BBY'
+      },
+      {
+        name: 'Yoda',
+        birthYear: undefined
+      }
+    ])
+  })
+```
+
+</details>
+
+Example at: [examples/reducer-conditional-operator.js](examples/reducer-conditional-operator.js)
+
 ### <a name="reducer-collection-mapping">Collection Mapping</a>
 
 Adding `[]` at the end of an entity reducer will map the given entity to each result of the current `acc.value` if `value` is a collection. If the value is not a collection, the entity will ignore the `[]` directive.
diff --git a/packages/data-point/examples/reducer-conditional-operator.js b/packages/data-point/examples/reducer-conditional-operator.js
new file mode 100644
index 0000000..eb9bb90
--- /dev/null
+++ b/packages/data-point/examples/reducer-conditional-operator.js
@@ -0,0 +1,39 @@
+const dataPoint = require('../').create()
+const assert = require('assert')
+
+const people = [
+  {
+    name: 'Luke Skywalker',
+    swapiId: '1'
+  },
+  {
+    name: 'Yoda',
+    swapiId: null
+  }
+]
+
+dataPoint.addEntities({
+  'request:getPerson': {
+    url: 'https://swapi.co/api/people/{value}'
+  },
+  'transform:getPerson': {
+    name: '$name',
+    // request:getPerson will only
+    // be executed if swapiId is
+    // not false, null or undefined
+    birthYear: '$swapiId | ?request:getPerson | $birth_year'
+  }
+})
+
+dataPoint.transform('transform:getPerson[]', people).then(acc => {
+  assert.deepEqual(acc.value, [
+    {
+      name: 'Luke Skywalker',
+      birthYear: '19BBY'
+    },
+    {
+      name: 'Yoda',
+      birthYear: undefined
+    }
+  ])
+})
diff --git a/packages/data-point/lib/entity-types/base-entity/resolve.js b/packages/data-point/lib/entity-types/base-entity/resolve.js
index 7f63498..41fd5fa 100644
--- a/packages/data-point/lib/entity-types/base-entity/resolve.js
+++ b/packages/data-point/lib/entity-types/base-entity/resolve.js
@@ -142,7 +142,14 @@ function resolveEntity (
 module.exports.resolveEntity = resolveEntity
 
 function resolve (manager, resolveReducer, accumulator, reducer, mainResolver) {
+  const hasEmptyConditional = reducer.hasEmptyConditional
+
+  if (hasEmptyConditional && utils.isFalsy(accumulator.value)) {
+    return Promise.resolve(accumulator)
+  }
+
   const resolveTransform = _.partial(resolveReducer, manager)
+
   const shouldMapCollection =
     reducer.asCollection && accumulator.value instanceof Array
 
@@ -158,6 +165,11 @@ function resolve (manager, resolveReducer, accumulator, reducer, mainResolver) {
 
   return Promise.map(accumulator.value, itemValue => {
     const itemCtx = utils.set(accumulator, 'value', itemValue)
+
+    if (hasEmptyConditional && utils.isFalsy(itemValue)) {
+      return Promise.resolve(itemCtx)
+    }
+
     return resolveEntity(
       manager,
       resolveTransform,
diff --git a/packages/data-point/lib/reducer-entity/factory.js b/packages/data-point/lib/reducer-entity/factory.js
index d86f5d4..bae8299 100644
--- a/packages/data-point/lib/reducer-entity/factory.js
+++ b/packages/data-point/lib/reducer-entity/factory.js
@@ -18,6 +18,7 @@ function ReducerEntity () {
   this.name = ''
   this.entityType = null
   this.asCollection = false
+  this.withEmptyConditional = false
 }
 
 module.exports.ReducerEntity = ReducerEntity
@@ -38,12 +39,16 @@ module.exports.isEntity = isEntity
  */
 function create (source) {
   const reducer = new ReducerEntity()
-
   const tokens = source.split(':')
 
-  reducer.entityType = tokens[0]
-  reducer.name = tokens[1].replace(/\[]$/, '')
-  reducer.asCollection = source.slice(-2) === '[]'
+  let entityType = tokens[0]
+  reducer.hasEmptyConditional = entityType.indexOf('?') === 0
+  reducer.entityType = entityType.replace(/^\?/, '')
+
+  let name = tokens[1]
+  reducer.asCollection = name.slice(-2) === '[]'
+  reducer.name = name.replace(/\[]$/, '')
+
   reducer.id = `${reducer.entityType}:${reducer.name}`
 
   return Object.freeze(reducer)
diff --git a/packages/data-point/lib/utils/index.js b/packages/data-point/lib/utils/index.js
index 9586b2b..cc8fe59 100644
--- a/packages/data-point/lib/utils/index.js
+++ b/packages/data-point/lib/utils/index.js
@@ -68,3 +68,9 @@ function inspect (acc, data) {
 }
 
 module.exports.inspect = inspect
+
+function isFalsy (val) {
+  return val === null || val === false || typeof val === 'undefined'
+}
+
+module.exports.isFalsy = isFalsy


Test patch:
diff --git a/packages/data-point/lib/entity-types/base-entity/resolve.test.js b/packages/data-point/lib/entity-types/base-entity/resolve.test.js
index 05dfe43..f2c30da 100644
--- a/packages/data-point/lib/entity-types/base-entity/resolve.test.js
+++ b/packages/data-point/lib/entity-types/base-entity/resolve.test.js
@@ -232,4 +232,38 @@ describe('ResolveEntity.resolve', () => {
       expect(acc).toHaveProperty('value', ['bar'])
     })
   })
+
+  test('It should not execute resolver if flag hasEmptyConditional is true and value is empty', () => {
+    const resolver = jest.fn()
+    return resolve(resolver)('?hash:asIs', undefined).then(acc => {
+      expect(resolver).not.toHaveBeenCalled()
+    })
+  })
+
+  test('It should execute resolver if flag hasEmptyConditional is true and value is not empty', () => {
+    const resolver = (acc, resolveTransform) => {
+      const result = utils.set(acc, 'value', 'bar')
+      return Promise.resolve(result)
+    }
+    return resolve(resolver)('?hash:asIs', 'foo').then(acc => {
+      expect(acc).toHaveProperty('value', 'bar')
+    })
+  })
+
+  test('It should execute resolver only on non empty items of collection if hasEmptyConditional is set', () => {
+    let count = 0
+    const resolver = (acc, resolveTransform) => {
+      const result = utils.set(acc, 'value', count++)
+      return Promise.resolve(result)
+    }
+    return resolve(resolver)('?hash:asIs[]', [
+      'a',
+      undefined,
+      'b',
+      null,
+      'c'
+    ]).then(acc => {
+      expect(acc).toHaveProperty('value', [0, undefined, 1, null, 2])
+    })
+  })
 })
diff --git a/packages/data-point/lib/reducer-entity/factory.test.js b/packages/data-point/lib/reducer-entity/factory.test.js
index b341cad..80c821b 100644
--- a/packages/data-point/lib/reducer-entity/factory.test.js
+++ b/packages/data-point/lib/reducer-entity/factory.test.js
@@ -15,6 +15,7 @@ test('reducer/reducer-entity#isEntity', () => {
   expect(factory.isEntity('a:abc')).toBe(true)
   expect(factory.isEntity('abc:a')).toBe(true)
   expect(factory.isEntity('abc:abc')).toBe(true)
+  expect(factory.isEntity('?abc:abc')).toBe(true)
   expect(factory.isEntity('abc:ab-c-')).toBe(true)
   expect(factory.isEntity('_abc:abc')).toBe(true)
   expect(factory.isEntity('#abc:abc')).toBe(true)
@@ -23,19 +24,40 @@ test('reducer/reducer-entity#isEntity', () => {
   expect(factory.isEntity('abc:abc[]d')).toBe(false)
 })
 
-test('reducer/reducer-entity#create', () => {
-  const reducer = factory.create('foo:abc')
-  expect(reducer.type).toBe('ReducerEntity')
-  expect(reducer.name).toBe('abc')
-  expect(reducer.entityType).toBe('foo')
-  expect(reducer.id).toBe('foo:abc')
-})
+describe('create', function () {
+  test('default create', () => {
+    const reducer = factory.create('foo:abc')
+    expect(reducer.hasEmptyConditional).toBe(false)
+    expect(reducer.asCollection).toBe(false)
+    expect(reducer.type).toBe('ReducerEntity')
+    expect(reducer.name).toBe('abc')
+    expect(reducer.entityType).toBe('foo')
+  })
+
+  test('as collection', () => {
+    const reducer = factory.create('foo:abc[]')
+    expect(reducer.asCollection).toBe(true)
+    expect(reducer.hasEmptyConditional).toBe(false)
+    expect(reducer.type).toBe('ReducerEntity')
+    expect(reducer.name).toBe('abc')
+    expect(reducer.entityType).toBe('foo')
+  })
+
+  test('with conditional', () => {
+    const reducer = factory.create('?foo:abc')
+    expect(reducer.hasEmptyConditional).toBe(true)
+    expect(reducer.asCollection).toBe(false)
+    expect(reducer.type).toBe('ReducerEntity')
+    expect(reducer.name).toBe('abc')
+    expect(reducer.entityType).toBe('foo')
+  })
 
-test('reducer/reducer-entity#create as collection of', () => {
-  const reducer = factory.create('foo:abc[]')
-  expect(reducer.asCollection).toBe(true)
-  expect(reducer.type).toBe('ReducerEntity')
-  expect(reducer.name).toBe('abc')
-  expect(reducer.entityType).toBe('foo')
-  expect(reducer.id).toBe('foo:abc')
+  test('with conditional and as collection', () => {
+    const reducer = factory.create('?foo:abc[]')
+    expect(reducer.hasEmptyConditional).toBe(true)
+    expect(reducer.asCollection).toBe(true)
+    expect(reducer.type).toBe('ReducerEntity')
+    expect(reducer.name).toBe('abc')
+    expect(reducer.entityType).toBe('foo')
+  })
 })
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
index 276696a..fc31da8 100644
--- a/packages/data-point/lib/reducer-object/__snapshots__/factory.test.js.snap
+++ b/packages/data-point/lib/reducer-object/__snapshots__/factory.test.js.snap
@@ -57,9 +57,11 @@ Array [
                   ReducerEntity {
                     "asCollection": false,
                     "entityType": "transform",
+                    "hasEmptyConditional": false,
                     "id": "transform:entity-name",
                     "name": "entity-name",
                     "type": "ReducerEntity",
+                    "withEmptyConditional": false,
                   },
                 ],
                 "typeOf": "TransformExpression",
diff --git a/packages/data-point/lib/utils/utils.test.js b/packages/data-point/lib/utils/utils.test.js
index 6da2b80..ab558f1 100644
--- a/packages/data-point/lib/utils/utils.test.js
+++ b/packages/data-point/lib/utils/utils.test.js
@@ -100,3 +100,15 @@ describe('inspect', () => {
     expect(console.info.mock.calls[2]).toContain('baz')
   })
 })
+
+describe('isEmpty', () => {
+  test('It should test for empty', () => {
+    expect(utils.isFalsy(null)).toEqual(true)
+    expect(utils.isFalsy(undefined)).toEqual(true)
+    expect(utils.isFalsy(false)).toEqual(true)
+    expect(utils.isFalsy([])).toEqual(false)
+    expect(utils.isFalsy('')).toEqual(false)
+    expect(utils.isFalsy(0)).toEqual(false)
+    expect(utils.isFalsy({})).toEqual(false)
+  })
+})