diff --git a/lib/execute-operator.js b/lib/execute-operator.js new file mode 100644 index 0000000..636d3d0 --- /dev/null +++ b/lib/execute-operator.js @@ -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 + ) +} diff --git a/lib/find-operator.js b/lib/find-operator.js new file mode 100644 index 0000000..8d3044a --- /dev/null +++ b/lib/find-operator.js @@ -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 + } +} diff --git a/lib/get-top-level-choices.js b/lib/get-top-level-choices.js deleted file mode 100644 index 750e964..0000000 --- a/lib/get-top-level-choices.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -const _ = require('lodash') -const operators = require('./operators') - -module.exports = function getTopLevelChoices (choices) { - const topLevelChoices = [] - - choices.forEach(choice => { - // Find first operator - let operator - let operatorValue - let operatorIsPath = false - - Object.entries(choice).forEach(([key, value]) => { - if (operator === undefined) { - if (key.endsWith('Path')) { - key = key.split('Path')[0] - operatorIsPath = true - } - - if (operators[key]) { - operator = operators[key] - operatorValue = value - } - } - }) - - topLevelChoices.push({ - operator, - operatorValue, - operatorIsPath, - definition: _.cloneDeep(choice) - }) - }) - - return topLevelChoices -} diff --git a/lib/index.js b/lib/index.js index 19654a0..2ef5abc 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 diff --git a/lib/operators/and.js b/lib/operators/and.js deleted file mode 100644 index db05090..0000000 --- a/lib/operators/and.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = function andOperator (inputValue, comparisonValue, candidateStateName, cache) { - let nextState - // TODO: The 'And' operator needs writing - return nextState -} diff --git a/lib/operators/index.js b/lib/operators/index.js index 2fe0a2f..9574a86 100644 --- a/lib/operators/index.js +++ b/lib/operators/index.js @@ -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'), diff --git a/lib/operators/not.js b/lib/operators/not.js deleted file mode 100644 index ec3eba6..0000000 --- a/lib/operators/not.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = function notOperator (inputValue, comparisonValue, candidateStateName, cache) { - let nextState - // TODO: The 'Not' operator needs writing - return nextState -} diff --git a/lib/operators/or.js b/lib/operators/or.js deleted file mode 100644 index 249be3f..0000000 --- a/lib/operators/or.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = function orOperator (inputValue, comparisonValue, candidateStateName, cache) { - let nextState - // TODO: The 'Or' operator needs writing - return nextState -} diff --git a/test/boolean-expr-tests.js b/test/boolean-expr-tests.js new file mode 100644 index 0000000..2024806 --- /dev/null +++ b/test/boolean-expr-tests.js @@ -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) + }) + } + }) + } +}) diff --git a/test/choice-tests.js b/test/data-test-expr-tests.js similarity index 54% rename from test/choice-tests.js rename to test/data-test-expr-tests.js index b9139f9..3def340 100644 --- a/test/choice-tests.js +++ b/test/data-test-expr-tests.js @@ -73,46 +73,48 @@ const tests = { ] } -for (const [operator, t] of Object.entries(tests)) { - describe(operator, () => { - for (const [input, comparisonValue, expected] of t) { - it(`Input: ${input} Comparison: ${comparisonValue}`, () => { - const calculateNextState = choiceProcessor( - { - Choices: [ - { - Variable: '$.foo', - [operator]: comparisonValue, - Next: 'NextState' - } - ], - Default: 'DefaultState' - } - ) - const result = calculateNextState({ foo: input }) - expect(result).to.eql(expected) - }) - } - }) +describe('Data-test expression', () => { + for (const [operator, t] of Object.entries(tests)) { + describe(operator, () => { + for (const [input, comparisonValue, expected] of t) { + it(`Input: ${input} Comparison: ${comparisonValue}`, () => { + const calculateNextState = choiceProcessor( + { + Choices: [ + { + Variable: '$.foo', + [operator]: comparisonValue, + Next: 'NextState' + } + ], + Default: 'DefaultState' + } + ) + const result = calculateNextState({ foo: input }) + expect(result).to.eql(expected) + }) + } + }) - describe(`${operator}Path`, () => { - for (const [input, comparisonValue, expected] of t) { - it(`Input: ${input} Comparison: ${comparisonValue}`, () => { - const calculateNextState = choiceProcessor( - { - Choices: [ - { - Variable: '$.foo', - [`${operator}Path`]: '$.comparison', - Next: 'NextState' - } - ], - Default: 'DefaultState' - } - ) - const result = calculateNextState({ foo: input, comparison: comparisonValue }) - expect(result).to.eql(expected) - }) - } - }) -} + describe(`${operator}Path`, () => { + for (const [input, comparisonValue, expected] of t) { + it(`Input: ${input} Comparison: ${comparisonValue}`, () => { + const calculateNextState = choiceProcessor( + { + Choices: [ + { + Variable: '$.foo', + [`${operator}Path`]: '$.comparison', + Next: 'NextState' + } + ], + Default: 'DefaultState' + } + ) + const result = calculateNextState({ foo: input, comparison: comparisonValue }) + expect(result).to.eql(expected) + }) + } + }) + } +})