Skip to content

Commit

Permalink
Merge pull request #211 from gtorodelvalle/feature/adding-state-to-at…
Browse files Browse the repository at this point in the history
…tribute-function-interpolator

[FEATURE] Including state in the attribute-function-interpolator
  • Loading branch information
gtorodelvalle authored Oct 28, 2016
2 parents e3a82c0 + d8a76d3 commit 35fef38
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGES_NEXT_RELEASE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [FEATURE] Including state management in the attribute-function-interpolator interpolator
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 41 additions & 3 deletions lib/interpolators/attributeFunctionInterpolator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -251,6 +288,7 @@ module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf
}
}
entityAttributeMap = getEntityAttributeMap(interpolationSpec);
stateMap = generateStateMap(interpolationSpec);
requestOptions = getRequestOptions(domainConf, contextBrokerConf, entityAttributeMap);
return deasync(attributeFunctionInterpolator);
};
129 changes: 128 additions & 1 deletion test/unit/interpolators/attributeFunctionInterpolator_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 35fef38

Please sign in to comment.