Skip to content

Commit

Permalink
Merge pull request #219 from gtorodelvalle/feature/global-state-for-a…
Browse files Browse the repository at this point in the history
…ttribute-function-interpolator

Maintaining global state between attribute-function-interpolators
  • Loading branch information
gtorodelvalle authored Oct 31, 2016
2 parents 27c1cbf + 670c9f1 commit aa4fb03
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGES_NEXT_RELEASE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [FEATURE] Maintaining global state between attribute-function-interpolator with distinct specifications
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ An example simulation configuration file is shown next to give you a glimpse of
The simulation configuration file accepts the following JSON properties or entries:

* **exports**: The FIWARE Device Simulation provides a templating mechanism to avoid repeating text into simulation configuration files as well as to facilitate the edition of these files. More information about this templating mechanism just after the description of the rest of the properties which may be used in a simulation configuration file.
* **globals**: An object including the global state variables and their initial values to be shared amongst executions of the `attribute-function-interpolator` no matter their specification. This property is related to the `attribute-function-interpolator` detailed below and it will become much clearer once the reader reaches that section.
* **require**: An array of names and/or paths of NPM packages to be required before running the simulation. This property is related to the `attribute-function-interpolator` detailed below. It makes it possible to `require()` these NPM packages directly in the code associated to these `attribute-function-interpolator`.
* **domain**: Includes information about the service and subservice (i.e., service path) to use in the requests. It is mandatory in case any `entities` are included in the simulation configuration (see below).
* **service**: The service to use in the requests.
Expand Down Expand Up @@ -339,15 +340,22 @@ 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.
* Sometimes it is useful to have access to the simulator date (mainly in case of fast-forward simulations (more information about fast-forward simulations below)), for that we inject into the Javascript code of `attribute-function-interpolator`s an object SimulationDate which behaves such as the Javascript `Date` object but "points" to the simulator time and date, this is `new SimulationDate()` returns the current `Date` for the current simulation. It is important to note that the `SimulationDate` object will only be available if you assign the result of your code evaluation to the `module.exports` property.
* A valid attribute value using the possibility to maitain state between `attribute-function-interpolator` interpolator executions is: `"attribute-function-interpolator(module.exports = new SimulationDate())"`, where the result of the evaluation (this is, the value assigned to `module.exports`) will be the current simulation date.
* In case you want to maintain state between `attribute-function-interpolator` interpolator executions, you can also do it following the next guidelines:
* Sometimes it is useful to have access to the simulation date (mainly in case of fast-forward simulations (more information about fast-forward simulations below)), for that we inject into the Javascript code of `attribute-function-interpolator`s an object SimulationDate which behaves such as the Javascript `Date` object but "points" to the simulation time and date, this is `new SimulationDate()` returns the current `Date` for the current simulation. It is important to note that the `SimulationDate` object will only be available if you assign the result of your code evaluation to the `module.exports` property.
* A valid attribute value using the possibility to access the current simulation time is: `"attribute-function-interpolator(module.exports = new SimulationDate())"`, where the result of the evaluation (this is, the value assigned to `module.exports`) will be the current simulation date.
* In case you want to maintain state amongst executions of `attribute-function-interpolator` with the same specification (this is, with the same Javascript code to be evaluated), you can 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.
* In case of managing state between `attribute-function-interpolator` execution, the `SimulationDate` previously mentioned is also available in the associated Javascript code.
* 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.
* A valid attribute value using the possibility to maintain state amongst `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.
* Last but not least, we have also incorporated the possibility to share state between distint `attribute-function-interpolator`s, this is, `attribute-function-interpolator`s with distinct associated Javascript code (since if it is the same, the state can be shared "locally" amongs all the instances of the same `attribute-function-interpolator` as previously described). To share state between distinct `attribute-function-interpolator`s no matter their specification or associated Javascript code, follow the next steps:
1. Assign initial values to the global state variables in the `globals` property of the simulation configuration. For example: `"globals": { "globalVar1": 1, "globalVar2": 2}`. This step is optional and its necessity depends on your specific `attribute-function-interpolator` Javascript code where, obviously, you should not access any non-declared variable.
2. The variables will be available to be used in all the `attribute-function-interpolator` instances of the simulation.
3. Return the result the evaluation setting it as the value for the `module.exports.result` property.
4. Return the global state variables whose values you would like to update as properties of the object assigned to `module.exports.state.globals`. The global variables will be updated accordingly and passed to the next `attribute-function-interpolator` being executed.
* A valid attribute value using the possibility to maintain global state between `attribute-function-interpolator` instances (no matter the Javascript code included in them): `"attribute-function-interpolator(module.exports = { result: ${{Entity:001}{active:001}} + globalVar1, state: { globals: { globalVar1: ++globalVar1 } }};)"`, 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 value of the `globalVar1` global state variable, which will be incremented in 1 and passed as incremented to the next execution of an `attribute-function-interpolator` interpolator.
* It is important to note that global state variables (this is, amongst `attribute-function-interpolator` instances no matter their specification or associated Javascript code) and local state variables (this is, amongst `attribute-function-interpolator` instances with the same specification or associated Javascript code) can be combined following the guidelines detailed above. Notice that local state variables will impose over global state varaiables. This is, if a `attribute-function-interpolator` uses a local state variable with the same name as a global state variable, the local one will preserve and apply.
* A valid attribute value using the possibility to maintain local and global state amongst `attribute-function-interpolator` interpolator executions is: `"attribute-function-interpolator(/* state: counter = 1 */ module.exports = { result: ${{Entity:001}{active:001}} + counter + globalVar1, state: { counter: ++counter, globals: { globalVar1: ++globalVar1 } } };)"`, 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 plus the value of the `globalVar1` state variable (which, on the other hand, is incremented globally in 1 before exiting the evaluation of the Javascript code).
* **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
11 changes: 11 additions & 0 deletions lib/fiwareDeviceSimulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,16 @@ function transpile(configuration, callback) {
});
}

/**
* Set the global variables to be passed amongs the attribute-function-interpolator interpolators
* @param {Object} configuration The simulation configuration
* @param {Function} callback The callback
*/
function setGlobals(configuration, callback) {
global.fdsGlobals = configuration.globals;
process.nextTick(callback.bind(null, null, configuration));
}

/**
* Imports the packages included in the require property of the simulation configuration
* @param {Object} configuration The simulation configuration
Expand Down Expand Up @@ -1268,6 +1278,7 @@ function start(config, theFromDate, theToDate, interval, margin, theDelay) {
},
async.apply(transpile, config),
fiwareDeviceSimulatorValidator.validateConfiguration,
setGlobals,
importPackages
], function(err, newConfig) {
if (err) {
Expand Down
60 changes: 49 additions & 11 deletions lib/interpolators/attributeFunctionInterpolator.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ function generateStateMap(spec) {
}
return stateMap;
}

module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf){
var entityAttributeMap,
requestOptions,
Expand All @@ -226,6 +225,50 @@ module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf
domainConf = theDomainConf;
contextBrokerConf = theContextBrokerConf;

/**
* Returns the variable map to be passed to the Javascript code to evaluated
* @return {Object} The variable map
*/
function getVariableMap() {
var variableMap = {}, globalNames, stateMapNames;
globalNames = Object.getOwnPropertyNames(global.fdsGlobals);
globalNames.forEach(function(globalName) {
variableMap[globalName] = global.fdsGlobals[globalName];
});
stateMapNames = Object.getOwnPropertyNames(stateMap);
stateMapNames.forEach(function(stateMapEntry) {
variableMap[stateMapEntry] = stateMap[stateMapEntry];
});
return variableMap;
}

/**
* Process the evaluated value returned by the Javascript code
* @param {Object} evaluatedValue The evaluated value
* @return {Object} The post-processed evaluated value
*/
function processEvaluatedValue(evaluatedValue) {
var globalNames, stateMapNames;
if (typeof evaluatedValue === 'object') {
if (evaluatedValue.state && typeof evaluatedValue.state === 'object') {
if (evaluatedValue.state.globals) {
globalNames = Object.getOwnPropertyNames(evaluatedValue.state.globals);
globalNames.forEach(function(globalName) {
global.fdsGlobals[globalName] = evaluatedValue.state.globals[globalName];
});
delete evaluatedValue.state.globals;
}
stateMapNames = Object.getOwnPropertyNames(evaluatedValue.state);
stateMapNames.forEach(function(stateMapEntry) {
stateMap[stateMapEntry] = evaluatedValue.state[stateMapEntry];
});
}
if (evaluatedValue.result || evaluatedValue.result === 0 || evaluatedValue.result === false) {
evaluatedValue = evaluatedValue.result;
}
}
return evaluatedValue;
}
/**
* Returns the new interpolated value asynchronously
* @return {Object} The new interpolated value
Expand Down Expand Up @@ -253,19 +296,14 @@ module.exports = function(interpolationSpec, theDomainConf, theContextBrokerConf
});
});

var evaluatedValue, evaluatedValueProps;
var evaluatedValue;
try {
if (evalStr.indexOf('module.exports') !== -1) {
global.SimulationDate = Date;
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;
}
}
global.fdsGlobals = global.fdsGlobals || {};
var variableMap = getVariableMap();
evaluatedValue = _eval(evalStr, variableMap, true);
evaluatedValue = processEvaluatedValue(evaluatedValue);
} else {
/* jshint evil: true */
evaluatedValue = eval(evalStr);
Expand Down
16 changes: 16 additions & 0 deletions lib/validators/fiwareDeviceSimulatorValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ function isEntities(simulationConfiguration) {
return false;
}

/**
* Checks if the globals information is valid
* @param {Object} simulationConfiguration The simulation configuration object
* @param {Function} callback The callback
*/
function validateGlobals(simulationConfiguration, callback) {
if (simulationConfiguration.globals) {
if (typeof simulationConfiguration.globals !== 'object') {
return callback(new fdsErrors.SimulationConfigurationNotValid('The \'require\' configuration information ' +
'is not an array of NPM packages names'));
}
}
return setImmediate(callback);
}

/**
* Checks if the require information is valid
* @param {Object} simulationConfiguration The simulation configuration object
Expand Down Expand Up @@ -801,6 +816,7 @@ function validateEntitiesConfiguration(simulationConfiguration, callback) {
function validateConfiguration(theSimulationConfiguration, callback) {
simulationConfiguration = theSimulationConfiguration;
async.series([
async.apply(validateGlobals, simulationConfiguration),
async.apply(validateRequire, simulationConfiguration),
async.apply(validateDomain, simulationConfiguration),
async.apply(validateContextBrokerConfiguration, simulationConfiguration),
Expand Down
Loading

0 comments on commit aa4fb03

Please sign in to comment.