diff --git a/lib/state-machines/state-types/Base-state.js b/lib/state-machines/state-types/Base-state.js index d882b0a5..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/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) { 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..cdc63c4f --- /dev/null +++ b/lib/state-machines/state-types/execution-context/index.js @@ -0,0 +1,14 @@ +const { DateTime } = require('luxon') + +class ExecutionContext { + constructor (execDesc) { + 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/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 d08ac1be..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 @@ -3,6 +3,9 @@ const Tokenizr = require('tokenizr') const PARAMLIST = 'param-list' const lexer = new Tokenizr() +lexer.rule(/^\$(\$.*$)/, (ctx, match) => { + ctx.accept('contextpath', match[1]) +}) 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[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 0a3737d7..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 @@ -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) } @@ -60,6 +60,7 @@ function makeDynamicHandler (params, references) { } // makeDynamicHandler const Evaluate = { + contextpath: evaluateContextObjectPath, path: evaluatePath, function: evaluateIntrinsic, string: token => token.value, @@ -68,27 +69,31 @@ 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 (token, _, executionContext) { + return evaluatePath(token, executionContext) +} // evaluateContextObjectPath + 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 new file mode 100644 index 00000000..b5fab7c5 --- /dev/null +++ b/test/context-object.js @@ -0,0 +1,60 @@ +/* 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: `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)) { + 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) + + const expected = (typeof result !== 'function') ? result : result(executionDescription) + expect(executionDescription.ctx).to.eql(expected) + }) // it ... +} + +function runStateMachine (statemachine) { + return statebox.startExecution( + {}, // input + statemachine, + { sendResponse: 'COMPLETE' } // options + ) +} 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/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/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 new file mode 100644 index 00000000..52dbb756 --- /dev/null +++ b/test/fixtures/state-machines/context-object/index.js @@ -0,0 +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'), + 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') +} 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) }