diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index e69de29..c2e642d 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -0,0 +1 @@ +- [FEATURE] Including state management in the attribute-function-interpolator interpolator diff --git a/README.md b/README.md index 5a42a93..fcd9ac9 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,12 @@ The simulation configuration file accepts the following JSON properties or entri 1. Include a `require` property in your simulation configuration file setting its value to an array including the names and/or paths of the NPM packages you will be using in any of your `attribute-function-interpolator` interpolators. These packages will be required before proceding with the simulation and made available to your `attribute-function-interpolator` code which uses them. For example: `"require": ["postfix-calculate"]`. 2. The result of the evaluation of your code should be assigned to the `module.exports` property (this is due to the fact that this functionality leans on the [`eval` NPM package](https://www.npmjs.com/package/eval) which imposes this restriction). * A valid attribute value using this advanced mode of the `attribute-function-interpolator` is: `"attribute-function-interpolator(var postfixCalculate = require('postfix-calculate'); module.exports = postfixCalculate('${{Entity:001}{active:001}} 1 +');)"`, where the result of the evaluation (this is, the value assigned to `module.exports`) will be the result of adding 1 to the value of the `active:001` attribute of the `Entity:001` entity, according to the [`postfix-calculate` NPM](https://www.npmjs.com/package/postfix-calculate) functionality. + * In case you want to maintain state between `attribute-function-interpolator` interpolator executions, you can also do it following the next guidelines: + 1. Include a comment in your `attribute-function-interpolator` Javascript code such as: `/* state: statefulVariable1 = 5, statefulVariable2 = {\"prop1\": \"value1\"}, statefulVariable3 */`, this is a `state:` tag followed by the list of variables you would like the interpolator to maintain as the state. This list is used to inject into your code these variables with the value included after the `=` character or `null` if no value is assigned for the first execution of your Javascript code. + 2. Return the result the evaluation setting it as the value for the `module.exports.result` property. + 3. Return the variables whose state should be maintained between executions of the interpolator as properties of an object assigned to the `module.exports.state` property. + * It is important to note that all the `attribute-function-interpolator` sharing the same specification (this is, your Javascript code) will share the same state. If you do not want this, just slightly change the specification somehow withouth affecting the execution of your code such adding an additional `;` or including a comment. + * A valid attribute value using the possibility to maitain state between `attribute-function-interpolator` interpolator executions is: `"attribute-function-interpolator(/* state: counter = 1 */ module.exports = { result: ${{Entity:001}{active:001}} + counter, state: { counter: ++counter }};)"`, where the result of the evaluation (this is, the value assigned to `module.exports.result`) will be the result of adding to the value of the `active:001` attribute of the `Entity:001` entity an increment equal to the times the interpolator has been run. * **metadata**: Array of metadata information to be associated to the attribute on the update. Each metadata array entry is an object including 3 properties: * **name**: The metadata name. * **type**: The metadata type. diff --git a/lib/interpolators/attributeFunctionInterpolator.js b/lib/interpolators/attributeFunctionInterpolator.js index c560a23..9bbe825 100644 --- a/lib/interpolators/attributeFunctionInterpolator.js +++ b/lib/interpolators/attributeFunctionInterpolator.js @@ -34,6 +34,7 @@ var domainConf, contextBrokerConf; var VARIABLE_RE = /\${{[^{}]+}{[^{}]+}}/g; var ENTITY_ATTRIBUTE_RE = /{[^{}]+}/g; +var STATE_RE = /\/\*\s*[^:]+:\s*(.*)\s*\*\//; /** * Returns the entity-attribute map for the passed spec @@ -190,9 +191,37 @@ function checkError(responseArray) { return false; } +/** + * Composes the state map from the state specification included in the interpolator specification + * @param {String} spec The interpolator specification + * @return {Object} The generated state map + */ +function generateStateMap(spec) { + var stateMap = {}, + stateRegExpResult = STATE_RE.exec(spec), + stateVariables, + stateVariablesArray; + if (stateRegExpResult && stateRegExpResult.length >= 1) { + stateVariables = stateRegExpResult[1]; + if (stateVariables) { + stateVariablesArray = stateVariables.split(','); + stateVariablesArray.forEach(function(variable) { + if (variable.indexOf('=') !== -1) { + stateMap[variable.trim().substring(0, variable.trim().indexOf('=')).trim()] = + JSON.parse(variable.trim().substring(variable.trim().indexOf('=') + 1)); + } else { + stateMap[variable.trim()] = null; + } + }); + } + } + return stateMap; +} + module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf){ var entityAttributeMap, - requestOptions; + requestOptions, + stateMap; domainConf = theDomainConf; contextBrokerConf = theContextBrokerConf; @@ -224,10 +253,18 @@ module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf }); }); - var evaluatedValue; + var evaluatedValue, evaluatedValueProps; try { if (evalStr.indexOf('module.exports') !== -1) { - evaluatedValue = _eval(evalStr, true); + evaluatedValue = _eval(evalStr, stateMap, true); + if (typeof evaluatedValue === 'object') { + evaluatedValueProps = Object.getOwnPropertyNames(evaluatedValue); + if (Object.getOwnPropertyNames(evaluatedValue).indexOf('result') !== -1 && + Object.getOwnPropertyNames(evaluatedValue).indexOf('state') !== -1) { + stateMap = evaluatedValue.state; + evaluatedValue = evaluatedValue.result; + } + } } else { /* jshint evil: true */ evaluatedValue = eval(evalStr); @@ -251,6 +288,7 @@ module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf } } entityAttributeMap = getEntityAttributeMap(interpolationSpec); + stateMap = generateStateMap(interpolationSpec); requestOptions = getRequestOptions(domainConf, contextBrokerConf, entityAttributeMap); return deasync(attributeFunctionInterpolator); }; diff --git a/test/unit/interpolators/attributeFunctionInterpolator_test.js b/test/unit/interpolators/attributeFunctionInterpolator_test.js index 2d11302..4199638 100644 --- a/test/unit/interpolators/attributeFunctionInterpolator_test.js +++ b/test/unit/interpolators/attributeFunctionInterpolator_test.js @@ -378,7 +378,134 @@ describe('attributeFunctionInterpolator tests', function() { } }); - it('should throw an error if the packages required in the interpolation specification are not evailable', + it('should pass the state and interpolate if it is used in the interpolation specification', function(done) { + try { + var attributeFunctionInterpolatorSpec = + '/* state: stateful1, stateful2 */ var linearInterpolator = require("' + ROOT_PATH + + '/lib/interpolators/linearInterpolator"); ' + + 'module.exports = { ' + + 'result: linearInterpolator([[0,0],[10,10]])(5) + (stateful1 = (stateful1 ? ++stateful1 : 1)) + ' + + '(stateful2 = (stateful2 ? ++stateful2 : 1)),' + + 'state: { stateful1: stateful1, stateful2: stateful2}' + + ' };'; + var + attributeFunctionInterpolatorFunction = + attributeFunctionInterpolator( + attributeFunctionInterpolatorSpec, + domain, contextBroker); + should(attributeFunctionInterpolatorFunction(token)).equal(7); + should(attributeFunctionInterpolatorFunction(token)).equal(9); + should(attributeFunctionInterpolatorFunction(token)).equal(11); + done(); + } catch(exception) { + done(exception); + } + }); + + it('should initiate (as a number), pass the state and interpolate if it is used in the interpolation specification', + function(done) { + try { + var attributeFunctionInterpolatorSpec = + '/* state: stateful1 = 5, stateful2 */ var linearInterpolator = require("' + ROOT_PATH + + '/lib/interpolators/linearInterpolator"); ' + + 'module.exports = { ' + + 'result: linearInterpolator([[0,0],[10,10]])(5) + stateful1 + ' + + '(stateful2 = (stateful2 ? ++stateful2 : 1)),' + + 'state: { stateful1: ++stateful1, stateful2: stateful2}' + + ' };'; + var + attributeFunctionInterpolatorFunction = + attributeFunctionInterpolator( + attributeFunctionInterpolatorSpec, + domain, contextBroker); + should(attributeFunctionInterpolatorFunction(token)).equal(11); + should(attributeFunctionInterpolatorFunction(token)).equal(13); + should(attributeFunctionInterpolatorFunction(token)).equal(15); + done(); + } catch(exception) { + done(exception); + } + } + ); + + it('should initiate (as a string), pass the state and interpolate if it is used in the interpolation specification', + function(done) { + try { + var attributeFunctionInterpolatorSpec = + '/* state: stateful1 = 5, stateful2 =\"tralara\" */ var linearInterpolator = require("' + ROOT_PATH + + '/lib/interpolators/linearInterpolator"); ' + + 'module.exports = { ' + + 'result: linearInterpolator([[0,0],[10,10]])(5) + (stateful2 === \"tralara\" ? stateful1 : 0),' + + 'state: { stateful1: ++stateful1, stateful2: \"\"}' + + ' };'; + var + attributeFunctionInterpolatorFunction = + attributeFunctionInterpolator( + attributeFunctionInterpolatorSpec, + domain, contextBroker); + should(attributeFunctionInterpolatorFunction(token)).equal(10); + should(attributeFunctionInterpolatorFunction(token)).equal(5); + should(attributeFunctionInterpolatorFunction(token)).equal(5); + done(); + } catch(exception) { + done(exception); + } + } + ); + + it('should initiate (as an array), pass the state and interpolate if it is used in the interpolation specification', + function(done) { + try { + var attributeFunctionInterpolatorSpec = + '/* state: stateful1 = [5], stateful2 */ var linearInterpolator = require("' + ROOT_PATH + + '/lib/interpolators/linearInterpolator"); ' + + 'module.exports = { ' + + 'result: linearInterpolator([[0,0],[10,10]])(5) + stateful1[0] + ' + + '(stateful2 = (stateful2 ? ++stateful2 : 1)),' + + 'state: { stateful1: [++stateful1], stateful2: stateful2}' + + ' };'; + var + attributeFunctionInterpolatorFunction = + attributeFunctionInterpolator( + attributeFunctionInterpolatorSpec, + domain, contextBroker); + should(attributeFunctionInterpolatorFunction(token)).equal(11); + should(attributeFunctionInterpolatorFunction(token)).equal(13); + should(attributeFunctionInterpolatorFunction(token)).equal(15); + done(); + } catch(exception) { + done(exception); + } + } + ); + + it('should initiate (as an array), pass the state and interpolate if it is used in the interpolation specification', + function(done) { + try { + var attributeFunctionInterpolatorSpec = + '/* state: stateful1 = {\"value\": 5}, stateful2 */ var linearInterpolator = require("' + ROOT_PATH + + '/lib/interpolators/linearInterpolator"); ' + + 'module.exports = { ' + + 'result: linearInterpolator([[0,0],[10,10]])(5) + stateful1.value + ' + + '(stateful2 = (stateful2 ? ++stateful2 : 1)),' + + 'state: { stateful1: {\"value\": ++stateful1.value}, stateful2: stateful2}' + + ' };'; + var + attributeFunctionInterpolatorFunction = + attributeFunctionInterpolator( + attributeFunctionInterpolatorSpec, + domain, contextBroker); + should(attributeFunctionInterpolatorFunction(token)).equal(11); + should(attributeFunctionInterpolatorFunction(token)).equal(13); + should(attributeFunctionInterpolatorFunction(token)).equal(15); + done(); + } catch(exception) { + done(exception); + } + } + ); + + it('should throw an error if the packages required in the interpolation specification are not available', function(done) { try { var