From 7fde89160bc93ce0f3e3be5c28a7508dc7f14544 Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 13:08:39 +0000 Subject: [PATCH 1/7] test: Context Object handling - first few tests --- test/context-object.js | 49 +++++++++++++++++++ .../context-object/built-ins/day-of-week.json | 12 +++++ .../built-ins/formatted-day-of-week.json | 12 +++++ .../built-ins/non-existant.json | 12 +++++ .../state-machines/context-object/index.js | 5 ++ test/pass-states.js | 10 ++-- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 test/context-object.js create mode 100644 test/fixtures/state-machines/context-object/built-ins/day-of-week.json create mode 100644 test/fixtures/state-machines/context-object/built-ins/formatted-day-of-week.json create mode 100644 test/fixtures/state-machines/context-object/built-ins/non-existant.json create mode 100644 test/fixtures/state-machines/context-object/index.js diff --git a/test/context-object.js b/test/context-object.js new file mode 100644 index 00000000..cd512594 --- /dev/null +++ b/test/context-object.js @@ -0,0 +1,49 @@ +/* eslint-env mocha */ + +const chai = require('chai') +const expect = chai.expect + +const contextObjectStateMachines = require('./fixtures/state-machines/context-object') + +const Statebox = require('./../lib') + +let statebox + +describe('Context Object', () => { + before('setup statebox', async () => { + statebox = new Statebox() + await statebox.ready + await statebox.createStateMachines(contextObjectStateMachines, {}) + }) + + const today = new Date().toLocaleDateString('en-EN', { weekday: 'long' }) + + const contextObjectStates = { + NonExistantProperty: { oops: null }, + DayOfWeek: { day: today }, + FormattedDayOfWeek: { day: `The day is ${today}` } + } + + for (const [name, result] of Object.entries(contextObjectStates)) { + test(name, result) + } +}) + +function test (statemachine, result) { + it(statemachine, async () => { + const executionDescription = await runStateMachine(statemachine) + + expect(executionDescription.status).to.eql('SUCCEEDED') + expect(executionDescription.stateMachineName).to.eql(statemachine) + expect(executionDescription.currentResource).to.eql(undefined) + expect(executionDescription.ctx).to.eql(result) + }) // it ... +} + +function runStateMachine (statemachine) { + return statebox.startExecution( + {}, // input + statemachine, + { sendResponse: 'COMPLETE' } // options + ) +} diff --git a/test/fixtures/state-machines/context-object/built-ins/day-of-week.json b/test/fixtures/state-machines/context-object/built-ins/day-of-week.json new file mode 100644 index 00000000..757d4fdf --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/day-of-week.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "day.$": "$$.DayOfWeek" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/built-ins/formatted-day-of-week.json b/test/fixtures/state-machines/context-object/built-ins/formatted-day-of-week.json new file mode 100644 index 00000000..1173f8a5 --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/formatted-day-of-week.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "day.$": "States.Format('Today is {}', $$.DayOfWeek)" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/built-ins/non-existant.json b/test/fixtures/state-machines/context-object/built-ins/non-existant.json new file mode 100644 index 00000000..77bc1afc --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/non-existant.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "oops.$": "$$.MissingProperty" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/index.js b/test/fixtures/state-machines/context-object/index.js new file mode 100644 index 00000000..29340c78 --- /dev/null +++ b/test/fixtures/state-machines/context-object/index.js @@ -0,0 +1,5 @@ +module.exports = { + NonExistantProperty: require('./built-ins/non-existant.json'), + DayOfWeek: require('./built-ins/day-of-week.json'), + FormattedDayOfWeek: require('./built-ins/formatted-day-of-week.json') +} diff --git a/test/pass-states.js b/test/pass-states.js index 264b01a7..4c1cbb07 100644 --- a/test/pass-states.js +++ b/test/pass-states.js @@ -166,12 +166,12 @@ function test (label, statemachine, input, result) { }) // it ... } -async function runStateMachine (statemachine, input) { - const executionDescription = await statebox.startExecution( +function runStateMachine (statemachine, input) { + return statebox.startExecution( Object.assign({}, input), statemachine, - {} // options + { + sendResponse: 'COMPLETE' + } // options ) - - return statebox.waitUntilStoppedRunning(executionDescription.executionName) } From e5744ee7b54dbb1d2955f029d835755ce9037fcd Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 13:09:57 +0000 Subject: [PATCH 2/7] fix: Remove Task context.resolveInputPaths Only used by the Finding state resources in tymly-core, which is where this code should have been all along. --- lib/state-machines/state-types/Task.js | 35 -------------------------- 1 file changed, 35 deletions(-) diff --git a/lib/state-machines/state-types/Task.js b/lib/state-machines/state-types/Task.js index 91f80294..bf6ae987 100644 --- a/lib/state-machines/state-types/Task.js +++ b/lib/state-machines/state-types/Task.js @@ -1,7 +1,5 @@ const BaseStateType = require('./Base-state') const boom = require('@hapi/boom') -const jp = require('jsonpath') -const _ = require('lodash') const debug = require('debug')('statebox') const States = require('./errors') @@ -38,41 +36,8 @@ class Context { return this.task.processTaskHeartbeat(output, this.executionName) .then(result => { this.heartbeat(result); return result }) } - - resolveInputPaths (input, template) { - const clonedInput = cloneOrDefault(input) - const clonedTemplate = cloneOrDefault(template) - resolvePaths(clonedInput, clonedTemplate) - return clonedTemplate - } } -function cloneOrDefault (obj) { - return (_.isObject(obj)) ? _.cloneDeep(obj) : { } -} // cloneOrDefault - -function resolvePaths (input, root) { - if (!_.isObject(root)) return - - // TODO: Support string-paths inside arrays - if (Array.isArray(root)) { - root.forEach(element => resolvePaths(input, element)) - return - } - - for (const [key, value] of Object.entries(root)) { - if (isJSONPath(value)) { - root[key] = jp.value(input, value) - } else { - resolvePaths(input, value) - } - } // for ... -} // resolvePaths - -function isJSONPath (p) { - return _.isString(p) && p.length !== 0 && p[0] === '$' -} // isJSONPath - /// ////////////////////////////////// class Task extends BaseStateType { constructor (stateName, stateMachine, stateDefinition, options) { From da607354e5f1b2a3edbd29a89bb716403e082c29 Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 13:10:44 +0000 Subject: [PATCH 3/7] Parse context object paths. Not yet properly evaluated. --- .../state-types/path-handlers/input-path-tokeniser.js | 6 ++++++ .../state-types/path-handlers/payload-template-handler.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js index d08ac1be..29a7b963 100644 --- a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js +++ b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js @@ -3,6 +3,9 @@ const Tokenizr = require('tokenizr') const PARAMLIST = 'param-list' const lexer = new Tokenizr() +lexer.rule(/^\$\$.*$/, (ctx, match) => { + ctx.accept('contextpath', match[0]) +}) lexer.rule(/^\$.*$/, (ctx, match) => { ctx.accept('path', match[0]) }) @@ -14,6 +17,9 @@ lexer.rule(/\)/, (ctx) => { ctx.accept('end-function', null) ctx.pop() }) +lexer.rule(PARAMLIST, /^\$\$.*$/, (ctx, match) => { + ctx.accept('contextpath', match[0]) +}) lexer.rule(PARAMLIST, /\$[^, )]*/, (ctx, match) => { ctx.accept('path', match[0]) }) diff --git a/lib/state-machines/state-types/path-handlers/payload-template-handler.js b/lib/state-machines/state-types/path-handlers/payload-template-handler.js index 0a3737d7..3bed7b7c 100644 --- a/lib/state-machines/state-types/path-handlers/payload-template-handler.js +++ b/lib/state-machines/state-types/path-handlers/payload-template-handler.js @@ -60,6 +60,7 @@ function makeDynamicHandler (params, references) { } // makeDynamicHandler const Evaluate = { + contextpath: evaluateContextObjectPath, path: evaluatePath, function: evaluateIntrinsic, string: token => token.value, @@ -77,6 +78,10 @@ function evaluateArgument (token, input) { return Evaluate[token.type](token, input) } // evaluateArgument +function evaluateContextObjectPath ({ value }, input) { + return `context object ${value}` +} + function evaluatePath ({ value }, input) { return extractValue(_.cloneDeep(jp.query(input, value))) } // evaluatePath From ab3dd2d9df049b098c04e5794ec6fe3119bc7e9b Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 14:26:54 +0000 Subject: [PATCH 4/7] Evaluation ContextObject paths, $$.somePath The specification talks about Context Object, but we already referred to the input as ctx, the context. Consequently, in our code I'm referring to the ExecutionContext rather than the context object. It's not the greatest name, but hopefully it's different enough from our existing names to be clear. --- lib/state-machines/state-types/Base-state.js | 2 +- .../path-handlers/input-path-handler.js | 2 +- .../path-handlers/input-path-tokeniser.js | 8 ++++---- .../path-handlers/payload-template-handler.js | 18 +++++++++--------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/state-machines/state-types/Base-state.js b/lib/state-machines/state-types/Base-state.js index d882b0a5..74b370bd 100644 --- a/lib/state-machines/state-types/Base-state.js +++ b/lib/state-machines/state-types/Base-state.js @@ -35,7 +35,7 @@ class BaseState { run (executionDescription) { try { - const input = this.inputSelector(executionDescription.ctx) + const input = this.inputSelector(executionDescription.ctx, {}) return this.process(executionDescription, input) } catch (e) { return this.processTaskFailure(e, executionDescription.executionName) diff --git a/lib/state-machines/state-types/path-handlers/input-path-handler.js b/lib/state-machines/state-types/path-handlers/input-path-handler.js index 2d9a8497..f98b6490 100644 --- a/lib/state-machines/state-types/path-handlers/input-path-handler.js +++ b/lib/state-machines/state-types/path-handlers/input-path-handler.js @@ -8,7 +8,7 @@ function inputPathHandler (inputPath, parameters) { const path = findSelector(inputPath) const parameterTemplate = payloadTemplateHandler(parameters) - return ctx => parameterTemplate(path(ctx)) + return (input, executionContext) => parameterTemplate(path(input), executionContext) } // inputPathHandler module.exports = inputPathHandler diff --git a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js index 29a7b963..31278338 100644 --- a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js +++ b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js @@ -3,8 +3,8 @@ const Tokenizr = require('tokenizr') const PARAMLIST = 'param-list' const lexer = new Tokenizr() -lexer.rule(/^\$\$.*$/, (ctx, match) => { - ctx.accept('contextpath', match[0]) +lexer.rule(/^\$(\$.*$)/, (ctx, match) => { + ctx.accept('contextpath', match[1]) }) lexer.rule(/^\$.*$/, (ctx, match) => { ctx.accept('path', match[0]) @@ -17,8 +17,8 @@ lexer.rule(/\)/, (ctx) => { ctx.accept('end-function', null) ctx.pop() }) -lexer.rule(PARAMLIST, /^\$\$.*$/, (ctx, match) => { - ctx.accept('contextpath', match[0]) +lexer.rule(PARAMLIST, /^\$(\$.*$)/, (ctx, match) => { + ctx.accept('contextpath', match[1]) }) lexer.rule(PARAMLIST, /\$[^, )]*/, (ctx, match) => { ctx.accept('path', match[0]) diff --git a/lib/state-machines/state-types/path-handlers/payload-template-handler.js b/lib/state-machines/state-types/path-handlers/payload-template-handler.js index 3bed7b7c..30eda8d4 100644 --- a/lib/state-machines/state-types/path-handlers/payload-template-handler.js +++ b/lib/state-machines/state-types/path-handlers/payload-template-handler.js @@ -48,10 +48,10 @@ function makeDynamicHandler (params, references) { const skeleton = skeletonizeParams(params, references) const replacers = makeReplacers(references) - return input => { + return (input, executionContext) => { const parameters = _.cloneDeep(skeleton) for (const [path, expr] of replacers) { - const extractedValue = evaluateExpression(expr, input) + const extractedValue = evaluateExpression(expr, input, executionContext) dottie.set(parameters, path, extractedValue) } @@ -69,18 +69,18 @@ const Evaluate = { null: () => null } // Evaluate -function evaluateExpression (expression, input) { +function evaluateExpression (expression, input, executionContext) { const token = argTokeniser(expression) - return evaluateArgument(token, input) + return evaluateArgument(token, input, executionContext) } // evaluateExpression -function evaluateArgument (token, input) { - return Evaluate[token.type](token, input) +function evaluateArgument (token, input, executionContext) { + return Evaluate[token.type](token, input, executionContext) } // evaluateArgument -function evaluateContextObjectPath ({ value }, input) { - return `context object ${value}` -} +function evaluateContextObjectPath (token, _, executionContext) { + return evaluatePath(token, executionContext) +} // evaluateContextObjectPath function evaluatePath ({ value }, input) { return extractValue(_.cloneDeep(jp.query(input, value))) From 558db819f9f67540c322d4bdce2283178f9e0dca Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 14:30:49 +0000 Subject: [PATCH 5/7] Evaluate against ExecutionContext object Currently only support DayOfWeek property --- lib/state-machines/state-types/Base-state.js | 3 ++- .../state-types/execution-context/index.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 lib/state-machines/state-types/execution-context/index.js diff --git a/lib/state-machines/state-types/Base-state.js b/lib/state-machines/state-types/Base-state.js index 74b370bd..2064eb66 100644 --- a/lib/state-machines/state-types/Base-state.js +++ b/lib/state-machines/state-types/Base-state.js @@ -3,6 +3,7 @@ const debugPackage = require('debug')('statebox') const _ = require('lodash') const Status = require('../../Status') const States = require('./errors') +const ExecutionContext = require('./execution-context') const PathHandlers = require('./path-handlers') @@ -35,7 +36,7 @@ class BaseState { run (executionDescription) { try { - const input = this.inputSelector(executionDescription.ctx, {}) + const input = this.inputSelector(executionDescription.ctx, ExecutionContext(executionDescription)) return this.process(executionDescription, input) } catch (e) { return this.processTaskFailure(e, executionDescription.executionName) diff --git a/lib/state-machines/state-types/execution-context/index.js b/lib/state-machines/state-types/execution-context/index.js new file mode 100644 index 00000000..b4d8bf0a --- /dev/null +++ b/lib/state-machines/state-types/execution-context/index.js @@ -0,0 +1,11 @@ +const { DateTime } = require('luxon') + +class ExecutionContext { + constructor (execDesc) { + this.execDesc = execDesc + } + + get DayOfWeek () { return DateTime.local().weekdayLong } +} + +module.exports = executionDescription => new ExecutionContext(executionDescription) From 899f9e26d1b6893347354ea63e134bacd6274bd3 Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 14:45:52 +0000 Subject: [PATCH 6/7] Evaluate ContextObject path as parameter to an instrinsic function --- .../state-types/path-handlers/input-path-tokeniser.js | 2 +- .../state-types/path-handlers/payload-template-handler.js | 4 ++-- test/context-object.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js index 31278338..37a3052c 100644 --- a/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js +++ b/lib/state-machines/state-types/path-handlers/input-path-tokeniser.js @@ -17,7 +17,7 @@ lexer.rule(/\)/, (ctx) => { ctx.accept('end-function', null) ctx.pop() }) -lexer.rule(PARAMLIST, /^\$(\$.*$)/, (ctx, match) => { +lexer.rule(PARAMLIST, /\$(\$[^, )]*)/, (ctx, match) => { ctx.accept('contextpath', match[1]) }) lexer.rule(PARAMLIST, /\$[^, )]*/, (ctx, match) => { diff --git a/lib/state-machines/state-types/path-handlers/payload-template-handler.js b/lib/state-machines/state-types/path-handlers/payload-template-handler.js index 30eda8d4..4718e42b 100644 --- a/lib/state-machines/state-types/path-handlers/payload-template-handler.js +++ b/lib/state-machines/state-types/path-handlers/payload-template-handler.js @@ -86,14 +86,14 @@ function evaluatePath ({ value }, input) { return extractValue(_.cloneDeep(jp.query(input, value))) } // evaluatePath -function evaluateIntrinsic (func, input) { +function evaluateIntrinsic (func, input, executionContext) { const fn = instrinsics[func.value] if (!fn) { ErrorStates.IntrinsicFailure.raise(`Unknown intrinsic States.${func.value}`) } try { - const values = func.parameters.map(token => evaluateArgument(token, input)) + const values = func.parameters.map(token => evaluateArgument(token, input, executionContext)) if (fn.validate) { fn.validate(func.parameters, values) diff --git a/test/context-object.js b/test/context-object.js index cd512594..e5b30be7 100644 --- a/test/context-object.js +++ b/test/context-object.js @@ -21,7 +21,7 @@ describe('Context Object', () => { const contextObjectStates = { NonExistantProperty: { oops: null }, DayOfWeek: { day: today }, - FormattedDayOfWeek: { day: `The day is ${today}` } + FormattedDayOfWeek: { day: `Today is ${today}` } } for (const [name, result] of Object.entries(contextObjectStates)) { From 4b5cffe09e7e8b863497fd88875ed05dee3fae1a Mon Sep 17 00:00:00 2001 From: Jez Higgins Date: Sat, 12 Dec 2020 15:24:12 +0000 Subject: [PATCH 7/7] Added some more execution context properties --- .../state-types/execution-context/index.js | 3 +++ test/context-object.js | 15 +++++++++++++-- .../context-object/built-ins/date.json | 12 ++++++++++++ .../context-object/built-ins/start-time.json | 12 ++++++++++++ .../context-object/built-ins/time.json | 12 ++++++++++++ .../state-machines/context-object/index.js | 6 +++++- 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/state-machines/context-object/built-ins/date.json create mode 100644 test/fixtures/state-machines/context-object/built-ins/start-time.json create mode 100644 test/fixtures/state-machines/context-object/built-ins/time.json diff --git a/lib/state-machines/state-types/execution-context/index.js b/lib/state-machines/state-types/execution-context/index.js index b4d8bf0a..cdc63c4f 100644 --- a/lib/state-machines/state-types/execution-context/index.js +++ b/lib/state-machines/state-types/execution-context/index.js @@ -5,7 +5,10 @@ class ExecutionContext { this.execDesc = execDesc } + get StartTimestamp () { return this.execDesc.startDate } get DayOfWeek () { return DateTime.local().weekdayLong } + get Time () { return DateTime.local().toLocaleString(DateTime.TIME_24_SIMPLE) } + get Date () { return DateTime.local().toLocaleString(DateTime.DATE_SHORT) } } module.exports = executionDescription => new ExecutionContext(executionDescription) diff --git a/test/context-object.js b/test/context-object.js index e5b30be7..b5fab7c5 100644 --- a/test/context-object.js +++ b/test/context-object.js @@ -21,7 +21,16 @@ describe('Context Object', () => { const contextObjectStates = { NonExistantProperty: { oops: null }, DayOfWeek: { day: today }, - FormattedDayOfWeek: { day: `Today is ${today}` } + FormattedDayOfWeek: { day: `Today is ${today}` }, + StartTime: eD => { return { startedAt: eD.startDate } }, + Time: eD => { + expect(eD.ctx.time).to.match(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/) + return eD.ctx + }, + Date: eD => { + expect(eD.ctx.date).to.match(/^\d\d\/\d\d\/\d\d\d\d$/) + return eD.ctx + } } for (const [name, result] of Object.entries(contextObjectStates)) { @@ -36,7 +45,9 @@ function test (statemachine, result) { expect(executionDescription.status).to.eql('SUCCEEDED') expect(executionDescription.stateMachineName).to.eql(statemachine) expect(executionDescription.currentResource).to.eql(undefined) - expect(executionDescription.ctx).to.eql(result) + + const expected = (typeof result !== 'function') ? result : result(executionDescription) + expect(executionDescription.ctx).to.eql(expected) }) // it ... } diff --git a/test/fixtures/state-machines/context-object/built-ins/date.json b/test/fixtures/state-machines/context-object/built-ins/date.json new file mode 100644 index 00000000..50a5be16 --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/date.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "date.$": "$$.Date" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/built-ins/start-time.json b/test/fixtures/state-machines/context-object/built-ins/start-time.json new file mode 100644 index 00000000..d81ca8ca --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/start-time.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "startedAt.$": "$$.StartTimestamp" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/built-ins/time.json b/test/fixtures/state-machines/context-object/built-ins/time.json new file mode 100644 index 00000000..59647b19 --- /dev/null +++ b/test/fixtures/state-machines/context-object/built-ins/time.json @@ -0,0 +1,12 @@ +{ + "StartAt": "First", + "States": { + "First": { + "Type": "Pass", + "Parameters": { + "time.$": "$$.Time" + }, + "End": true + } + } +} diff --git a/test/fixtures/state-machines/context-object/index.js b/test/fixtures/state-machines/context-object/index.js index 29340c78..52dbb756 100644 --- a/test/fixtures/state-machines/context-object/index.js +++ b/test/fixtures/state-machines/context-object/index.js @@ -1,5 +1,9 @@ module.exports = { NonExistantProperty: require('./built-ins/non-existant.json'), DayOfWeek: require('./built-ins/day-of-week.json'), - FormattedDayOfWeek: require('./built-ins/formatted-day-of-week.json') + FormattedDayOfWeek: require('./built-ins/formatted-day-of-week.json'), + StartTime: require('./built-ins/start-time.json'), + Time: require('./built-ins/time.json'), + Date: require('./built-ins/date.json'), + Timestamp: require('./built-ins/time.json') }