Skip to content

Commit

Permalink
feat: refactor and enable And, Not, Or operators with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
meenahoda authored and Meena Brend committed Oct 29, 2020
1 parent dca19fa commit 936bb39
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 122 deletions.
20 changes: 20 additions & 0 deletions lib/execute-operator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const findOperator = require('./find-operator')

const isJSONPath = p => (p && typeof p.valueOf() === 'string') && p.length !== 0 && p[0] === '$'

module.exports = function executeOperator (choice, inputCache, values) {
const operator = findOperator(choice)

const inputValue = inputCache.get(choice.Variable, values)

if (operator.isPath && isJSONPath(operator.value)) {
operator.value = inputCache.get(operator.value, values)
}

return operator.operator(
inputValue,
operator.value,
choice.Next,
inputCache
)
}
27 changes: 27 additions & 0 deletions lib/find-operator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const operators = require('./operators')

module.exports = function findOperator (choice) {
let operator
let value
let isPath = false

Object.entries(choice).forEach(([k, v]) => {
if (operator === undefined) {
if (k.endsWith('Path')) {
k = k.split('Path')[0]
isPath = true
}

if (operators[k]) {
operator = operators[k]
value = v
}
}
})

return {
operator,
value,
isPath
}
}
38 changes: 0 additions & 38 deletions lib/get-top-level-choices.js

This file was deleted.

43 changes: 25 additions & 18 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
'use strict'

const InputValueCache = require('./Input-value-cache')
const getTopLevelChoices = require('./get-top-level-choices')
const isJSONPath = p => (p && typeof p.valueOf() === 'string') && p.length !== 0 && p[0] === '$'
const executeOperator = require('./execute-operator')

module.exports = function (definition) {
const choices = getTopLevelChoices(definition.Choices)

return function calculateNextState (values) {
const inputCache = new InputValueCache()

for (const choice of choices) {
const inputValue = inputCache.get(choice.definition.Variable, values)

if (choice.operatorIsPath && isJSONPath(choice.operatorValue)) {
choice.operatorValue = inputCache.get(choice.operatorValue, values)
for (const choice of definition.Choices) {
if (choice.Not) {
const nextState = executeOperator({ ...choice.Not, Next: choice.Next }, inputCache, values)

if (!nextState) return choice.Next
} else if (choice.And || choice.Or) {
const allChoices = choice.And || choice.Or
const allNextStates = allChoices.map(c => executeOperator({ ...c, Next: choice.Next }, inputCache, values))

if (choice.And) {
const allEqual = arr => arr.every(v => v === arr[0])

if (allEqual(allNextStates) && allNextStates[0] === choice.Next) {
return choice.Next
}
} else if (choice.Or) {
if (allNextStates.includes(choice.Next)) {
return choice.Next
}
}
} else {
const nextState = executeOperator(choice, inputCache, values)

if (nextState) return nextState
}

const nextState = choice.operator(
inputValue,
choice.operatorValue,
choice.definition.Next,
inputCache
)

if (nextState) return nextState
}

return definition.Default ? definition.Default : null
Expand Down
7 changes: 0 additions & 7 deletions lib/operators/and.js

This file was deleted.

3 changes: 0 additions & 3 deletions lib/operators/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
module.exports = {
And: require('./and'),
BooleanEquals: require('./boolean-equals'),
Includes: require('./includes'),
IsUndefined: require('./is-undefined'),
IsNull: require('./is-null'),
Not: require('./not'),
NumericEquals: require('./numeric-equals'),
NumericGreaterThan: require('./numeric-greater-than'),
NumericGreaterThanEquals: require('./numeric-greater-than-equals'),
NumericLessThan: require('./numeric-less-than'),
NumericLessThanEquals: require('./numeric-less-than-equals'),
Or: require('./or'),
StringEquals: require('./string-equals'),
StringGreaterThan: require('./string-greater-than'),
StringGreaterThanEquals: require('./string-greater-than-equals'),
Expand Down
7 changes: 0 additions & 7 deletions lib/operators/not.js

This file was deleted.

7 changes: 0 additions & 7 deletions lib/operators/or.js

This file was deleted.

208 changes: 208 additions & 0 deletions test/boolean-expr-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/* eslint-env mocha */

'use strict'

const chai = require('chai')
const expect = chai.expect

// As inspired-by: http://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-choice-state.html#amazon-states-language-choice-state-rules

const choiceProcessor = require('./../lib')

const tests = {
Not: [
{
choices: {
Choices: [
{
Not: {
Variable: '$.type',
StringEquals: 'Private'
},
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { type: 'Public' },
expected: 'NextState'
},
{
choices: {
Choices: [
{
Not: {
Variable: '$.type',
StringEquals: 'Private'
},
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { type: 'Private' },
expected: 'DefaultState'
}
],
And: [
{
choices: {
Choices: [
{
And: [
{
Variable: '$.value',
NumericGreaterThan: 20
},
{
Variable: '$.value',
NumericLessThan: 25
}
],
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { value: 21 },
expected: 'NextState'
},
{
choices: {
Choices: [
{
And: [
{
Variable: '$.value',
NumericGreaterThan: 20
},
{
Variable: '$.value',
NumericLessThan: 25
}
],
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { value: 20 },
expected: 'DefaultState'
},
{
choices: {
Choices: [
{
And: [
{
Variable: '$.value',
NumericGreaterThan: 20
},
{
Variable: '$.value',
NumericLessThan: 25
}
],
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { value: 28 },
expected: 'DefaultState'
}
],
Or: [
{
choices: {
Choices: [
{
Or: [
{
Variable: '$.value',
NumericGreaterThan: 22
},
{
Variable: '$.value',
NumericLessThan: 25
}
],
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { value: 22 },
expected: 'NextState'
},
{
choices: {
Choices: [
{
Or: [
{
Variable: '$.value',
NumericGreaterThan: 22
},
{
Variable: '$.value',
NumericLessThan: 25
}
],
Next: 'NextState'
}
],
Default: 'DefaultState'
},
input: { value: 25 },
expected: 'NextState'
}
],
Mixed: [
{
choices: {
Choices: [
{
Not: {
Variable: '$.type',
StringEquals: 'Private'
},
Next: 'Public'
},
{
And: [
{
Variable: '$.value',
NumericGreaterThanEquals: 20
},
{
Variable: '$.value',
NumericLessThan: 30
}
],
Next: 'ValueInTwenties'
}
],
Default: 'RecordEvent'
},
input: { type: 'Private', value: 22 },
expected: 'ValueInTwenties'
}
]
}

describe('Boolean expression', () => {
for (const [operator, t] of Object.entries(tests)) {
describe(operator, () => {
let i = 0
for (const { choices, input, expected } of t) {
i++
it(`${i}`, () => {
const calculateNextState = choiceProcessor(choices)
const result = calculateNextState(input)
expect(result).to.eql(expected)
})
}
})
}
})
Loading

0 comments on commit 936bb39

Please sign in to comment.