From 5248c3ba4e697c1e0b607d28f8bf5738b500063a Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Mon, 5 Aug 2019 15:59:56 +0200 Subject: [PATCH 1/4] add support for jexl as additional expression lanaguage * JEXL (https://github.com/TomFrost/jexl) a library that provides a reich javascript expression language. * update docs * add tests for ngsi & ngsi v2 * add logger to report about missing contexts in expression * add also object_id to context, so that object_id can be used in expressions * default expression language is defined with `defaultExpressionLanguage: legacy|jexl` * per device expression language is defined with `expressionLanguage: legacy|jexl` in the device descriptor --- CHANGES_NEXT_RELEASE | 3 +- README.md | 2 +- doc/expressionLanguage.md | 89 +- doc/installationguide.md | 5 + lib/commonConfig.js | 8 +- lib/fiware-iotagent-lib.js | 25 +- lib/model/Device.js | 3 +- lib/model/Group.js | 3 +- lib/plugins/expressionPlugin.js | 28 +- lib/plugins/jexlParser.js | 188 ++++ lib/templates/createDevice.json | 7 +- lib/templates/deviceGroup.json | 4 + lib/templates/updateDevice.json | 3 +- package.json | 1 + .../expressionCombinedTransformations-test.js | 285 ++++++ .../jexlBasedTransformations-test.js | 305 +++++++ test/unit/expressions/jexlExpression-test.js | 240 +++++ .../expressionCombinedTransformations-test.js | 287 ++++++ .../jexlBasedTransformations-test.js | 854 ++++++++++++++++++ 19 files changed, 2323 insertions(+), 17 deletions(-) create mode 100644 lib/plugins/jexlParser.js create mode 100644 test/unit/expressions/expressionCombinedTransformations-test.js create mode 100644 test/unit/expressions/jexlBasedTransformations-test.js create mode 100644 test/unit/expressions/jexlExpression-test.js create mode 100644 test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js create mode 100644 test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index ec7d04d4c..93f989f15 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,4 +1,5 @@ Refresh Documentation Add NGSIv2 metadata support to device provisioned attributes Fix: Error message when sending measures with unknown/undefined attribute -Add Null check within executeWithSecurity() to avoid crash (#829) \ No newline at end of file +Add Null check within executeWithSecurity() to avoid crash (#829) +Add support for JEXL as expression language (#801) diff --git a/README.md b/README.md index e3499e6e7..76f51a3df 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ decommission devices. ## Testing -Contribitions to development can be found [here](doc/development.md) - additional contributions are welcome. +Contributions to development can be found [here](doc/development.md) - additional contributions are welcome. ### Agent Console diff --git a/doc/expressionLanguage.md b/doc/expressionLanguage.md index 362bc5d5d..8fc42798e 100644 --- a/doc/expressionLanguage.md +++ b/doc/expressionLanguage.md @@ -16,8 +16,8 @@ The IoTAgent Library provides an expression language for measurement transformation, that can be used to adapt the information coming from the South Bound APIs to the information reported to the Context Broker. Expressions in this -language can be configured for provisioned attributes as explained in the Device Provisioning API section in the main -README.md. +language can be configured for provisioned attributes as explained in the Device Provisioning API section in the +main README.md. ## Measurement transformation @@ -301,3 +301,88 @@ two possible types of expressions: Integer (arithmetic operations) or Strings. - update (of type "Boolean"): true -> ${@update * 20} -> ${ 1 \* 20 } -> $ { 20 } -> $ { "20"} -> False - update (of type "Boolean"): false -> ${@update * 20} -> ${ 0 \* 20 } -> $ { 0 } -> $ { "0"} -> False - update (of type "Boolean"): true -> ${trim(@updated)} -> ${trim("true")} -> $ { "true" } -> $ { "true"} -> True + +## JEXL Based Transformations + +As an alternative, the IoTAgent Library supports as well [JEXL](https://github.com/TomFrost/jexl). +To use JEXL, you will need to either configure it as default +language using the `defaultExpressionLanguage` field to `jexl` or configuring +the usage of JEXL as expression language for a given device: + +``` +{ + "devices":[ + { + "device_id":"45", + "protocol":"GENERIC_PROTO", + "entity_name":"WasteContainer:WC45", + "entity_type":"WasteContainer", + "expressionLanguage": "jexl", + "attributes":[ + { + "name":"location", + "type":"geo:point", + "expression": "..." + }, + { + "name":"fillingLevel", + "type":"Number", + "expression": "..." + } + ] + } + ] +} +``` + +In the following we provide examples of using JEXL to apply transformations. + +### Quick comparison to default language + +* JEXL supports the following types: Boolean, String, Number, Object, Array. +* JEXL allows to navigate and [filter](https://github.com/TomFrost/jexl#collections) objects and arrays. +* JEXL supports if..then...else... via [ternary operator](https://github.com/TomFrost/jexl#ternary-operator). +* JEXL additionally supports the following operations: + Divide and floor `//`, Modulus `%`, Logical AND `&&` and + Logical OR `||`. Negation operator is `!` +* JEXL supports [comparisons](https://github.com/TomFrost/jexl#comparisons). + + +For more details, check JEXL language details +[here](https://github.com/TomFrost/jexl#all-the-details). + +### Examples of expressions + +The following table shows expressions and their expected outcomes for a measure +with four attributes: "value" with value 6, "name" with value "DevId629", +an object with value {name: "John", surname: "Doe"} and array of value [1,3]. + +| Expression | Expected outcome | +|:--------------------------- |:--------------------- | +| '5 * value' | 30 | +| '(6 + value) * 3' | 36 | +| 'value / 12 + 1' | 1.5 | +| '(5 + 2) * (value + 7)' | 91 | +| 'value * 5.2' | 31.2 | +| '"Pruebas " + "De Strings"' | 'Pruebas De Strings' | +| 'name + "value is " +value' | 'DevId629 value is 6' | + +Support for `trim`, `length`, `substr` and `indexOf` transformations was added. + +| Expression | Expected outcome | +|:--------------------------- |:--------------------- | +| '" a "|trim' | 'a' | +| 'name|length' | 8 | +| 'name|indexOf("e")' | 1 | +| 'name|substring(0,name|indexOf("e")+1)'| 'De' | + +The following are some expressions not supported by the legacy expression +language: + +| Expression | Expected outcome | +|:--------------------------- |:--------------------- | +| 'value == 6? true : false' | true | +| 'value == 6 && name|indexOf("e")>0| true | +| 'array[1]+1' | 3 | +| 'object.name' | 'John' | +| '{type:"Point",coordinates: [value,value]}' | {type:"Point",coordinates: [6,6]}| diff --git a/doc/installationguide.md b/doc/installationguide.md index cc9092e14..f76f98102 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -210,6 +210,10 @@ used for the same purpose. For instance: the IoTAgent runs in a single thread. For more details about multi-core functionality, please refer to the [Cluster](https://nodejs.org/api/cluster.html) module in Node.js and [this section](howto.md#iot-agent-in-multi-thread-mode) of the library documentation. +- **defaultExpressionLanguage**: the default expression language used to + compute expressions, possible values are: `legacy|jexl`. When not set or + wrongly set, `legacy` is used as default value. + ### Configuration using environment variables @@ -262,3 +266,4 @@ overrides. | IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` | | IOTA_AUTOCAST | `autocast` | | IOTA_MULTI_CORE | `multiCore` | +| IOTA_DEFAULT_EXPRESSION_LANGUAGE | defaultExpressionLanguage | diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 13309ef09..c6dc5bc3f 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -91,7 +91,8 @@ function processEnvironmentVariables() { 'IOTA_APPEND_MODE', 'IOTA_POLLING_EXPIRATION', 'IOTA_POLLING_DAEMON_FREQ', - 'IOTA_MULTI_CORE' + 'IOTA_MULTI_CORE', + 'IOTA_DEFAULT_EXPRESSION_LANGUAGE' ], iotamVariables = [ 'IOTA_IOTAM_URL', @@ -322,6 +323,11 @@ function processEnvironmentVariables() { } else { config.multiCore = config.multiCore === true; } + + if (process.env.IOTA_DEFAULT_EXPRESSION_LANGUAGE) { + config.defaultExpressionLanguage = process.env.IOTA_DEFAULT_EXPRESSION_LANGUAGE; + } + } function setConfig(newConfig) { diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index c315a8a8b..a045e9961 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -84,6 +84,16 @@ function doActivate(newConfig, callback) { commandRegistry, securityService; + logger.format = logger.formatters.pipe; + + config.setConfig(newConfig); //moving up here because otherwise env variable are not considered by the code below + + if (!config.getConfig().dieOnUnexpectedError) { + process.on('uncaughtException', globalErrorHandler); + } + + newConfig = config.getConfig(); + if (newConfig.contextBroker) { if (! newConfig.contextBroker.url && newConfig.contextBroker.host && newConfig.contextBroker.port) { newConfig.contextBroker.url = 'http://' + newConfig.contextBroker.host + ':' + newConfig.contextBroker.port; @@ -113,7 +123,16 @@ function doActivate(newConfig, callback) { } } - config.setConfig(newConfig); + if (newConfig.defaultExpressionLanguage && + (newConfig.defaultExpressionLanguage === 'legacy' || + newConfig.defaultExpressionLanguage ==='jexl')){ + logger.info(context, 'Using ' + newConfig.defaultExpressionLanguage + ' as default expression language'); + } else { + logger.info(context, 'Default expression language not set, or invalid, using legacy configuration'); + newConfig.defaultExpressionLanguage = 'legacy'; + } + + config.setConfig(newConfig); //after chaging some configuration, we re apply the configuration logger.info(context, 'Activating IOT Agent NGSI Library.'); @@ -156,10 +175,6 @@ function doActivate(newConfig, callback) { ], callback); }; - if (!config.getConfig().dieOnUnexpectedError) { - process.on('uncaughtException', globalErrorHandler); - } - config.setSecurityService(securityService); config.setRegistry(registry); config.setGroupRegistry(groupRegistry); diff --git a/lib/model/Device.js b/lib/model/Device.js index a367126e3..071dfc4c8 100644 --- a/lib/model/Device.js +++ b/lib/model/Device.js @@ -48,7 +48,8 @@ var Device = new Schema({ internalId: String, creationDate: { type: Date, default: Date.now }, internalAttributes: Object, - autoprovision: Boolean + autoprovision: Boolean, + expressionLanguage: String }); function load(db) { diff --git a/lib/model/Group.js b/lib/model/Group.js index 447524d4c..8754981b6 100644 --- a/lib/model/Group.js +++ b/lib/model/Group.js @@ -41,7 +41,8 @@ var Group = new Schema({ lazy: Array, attributes: Array, internalAttributes: Array, - autoprovision: Boolean + autoprovision: Boolean, + expressionLanguage: String }); function load(db) { diff --git a/lib/plugins/expressionPlugin.js b/lib/plugins/expressionPlugin.js index 8ed729556..f4224886e 100644 --- a/lib/plugins/expressionPlugin.js +++ b/lib/plugins/expressionPlugin.js @@ -22,12 +22,14 @@ * please contact with::daniel.moranjimenez@telefonica.com * * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Federico M. Facca - Martel Innovate */ 'use strict'; var _ = require('underscore'), - parser = require('./expressionParser'), + legacyParser = require('./expressionParser'), + jexlParser = require('./jexlParser'), config = require('../commonConfig'), /*jshint unused:false*/ logger = require('logops'), @@ -62,7 +64,27 @@ function mergeAttributes(attrList1, attrList2) { function update(entity, typeInformation, callback) { + + function checkJexl(typeInformation){ + if (config.getConfig().defaultExpressionLanguage === 'jexl' && + typeInformation.expressionLanguage && + typeInformation.expressionLanguage !== 'legacy') { + return true; + } else if (config.getConfig().defaultExpressionLanguage === 'jexl' && + !typeInformation.expressionLanguage) { + return true; + } else if (config.getConfig().defaultExpressionLanguage === 'legacy' && + typeInformation.expressionLanguage && typeInformation.expressionLanguage === 'jexl') { + return true; + } + return false; + } + function processEntityUpdateNgsi1(entity) { + var parser = legacyParser; + if (checkJexl(typeInformation)){ + parser = jexlParser; + } var expressionAttributes = [], ctx = parser.extractContext(entity.attributes); @@ -76,6 +98,10 @@ function update(entity, typeInformation, callback) { } function processEntityUpdateNgsi2(attributes) { + var parser = legacyParser; + if (checkJexl(typeInformation)){ + parser = jexlParser; + } var expressionAttributes = [], ctx = parser.extractContext(attributes); diff --git a/lib/plugins/jexlParser.js b/lib/plugins/jexlParser.js new file mode 100644 index 000000000..176b1feb8 --- /dev/null +++ b/lib/plugins/jexlParser.js @@ -0,0 +1,188 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Developed by: Federico M. Facca - Martel Innovate + */ + +'use strict'; + +const jexl = require('jexl'); +var errors = require('../errors'), + logger = require('logops'), + config = require('../commonConfig'), + logContext = { + op: 'IoTAgentNGSI.JEXL' + }; + + +jexl.addTransform('indexOf', (val, char) => String(val).indexOf(char)); +jexl.addTransform('length', (val) => String(val).length); +jexl.addTransform('trim', (val) => String(val).trim()); +jexl.addTransform('substr', (val, int1, int2) => String(val).substr(int1, int2)); + +function parse(expression, context, callback) { + var result, + error; + + try { + + result = jexl.evalSync (expression, context); + + } catch (e) { + error = new errors.InvalidExpression(expression); + if (callback) { + callback(error); + } else { + throw error; + } + } + + if (callback) { + callback(null, result); + } else { + return result; + } +} + +function extractContext(attributeList) { + var context = {}; + var value; + + for (var i = 0; i < attributeList.length; i++) { + if (Number.parseInt(attributeList[i].value)) { + value = Number.parseInt(attributeList[i].value); + } else if (Number.parseFloat(attributeList[i].value)) { + value = Number.parseFloat(attributeList[i].value); + } else if (String(attributeList[i].value) === 'true') { + value = true; + } else if (String(attributeList[i].value) === 'false') { + value = false; + } else { + value = attributeList[i].value; + } + if(attributeList[i].name) { + context[attributeList[i].name] = value; + } + /*jshint camelcase: false */ + if(attributeList[i].object_id) { + context[attributeList[i].object_id] = value; + } + /*jshint camelcase: true */ + } + + return context; +} + +function applyExpression(expression, context, typeInformation) { + return jexl.evalSync (expression, context); +} + +function expressionApplier(context, typeInformation) { + return function(attribute) { + + /** + * Determines if a value is of type float + * + * @param {String} value Value to be analyzed + * @return {boolean} True if float, False otherwise. + */ + function isFloat(value) { + return !isNaN(value) && value.toString().indexOf('.') !== -1; + } + + var newAttribute = { + name: attribute.name, + type: attribute.type + }; + + /*jshint camelcase: false */ + if (config.checkNgsi2() && attribute.object_id) { + newAttribute.object_id = attribute.object_id; + } + + newAttribute.value = applyExpression(attribute.expression, context, typeInformation); + + if (attribute.type === 'Number' && isFloat(newAttribute.value)) { + newAttribute.value = Number.parseFloat(newAttribute.value); + } + else if (attribute.type === 'Number' && Number.parseInt(newAttribute.value)) { + newAttribute.value = Number.parseInt(newAttribute.value); + } + else if (attribute.type === 'Boolean') { + newAttribute.value = (newAttribute.value === 'true' || newAttribute.value === '1'); + } + else if (attribute.type === 'None') { + newAttribute.value = null; + } + else if (attribute.type === 'Text') { + newAttribute.value = String(newAttribute.value); + } + else if (attribute.type !== 'StructuredValue') { + newAttribute.value = String(newAttribute.value); + } + + return newAttribute; + }; +} + +function isTransform(identifier) { + return jexl.getTransform(identifier) === ( null || undefined) ? false : true; +} + +function contextAvailable(expression, context) { + var error; + try{ + var lexer = jexl._getLexer(); + var identifiers = lexer.tokenize(expression).filter(function(token) { + return token.type === 'identifier'; + }); + var keys = Object.keys(context); + var validContext = true; + identifiers.some(function(element) { + if (!keys.includes(element.value) && !isTransform(element.value)) { + validContext = false; + logger.warn(logContext, 'For expression "[%s]" context does not have element %s' , expression, element.value); + } + return validContext === false; + }); + if(validContext) { + jexl.evalSync (expression, context); + } + return validContext; + } catch (e) { + error = new errors.InvalidExpression(expression); + throw error; + } +} + +function processExpressionAttributes(typeInformation, list, context) { + return list + .filter(function(item) { + return item.expression && contextAvailable(item.expression, context); + }) + .map(expressionApplier(context, typeInformation)); +} + +exports.extractContext = extractContext; +exports.processExpressionAttributes = processExpressionAttributes; +exports.applyExpression = applyExpression; +exports.parse = parse; diff --git a/lib/templates/createDevice.json b/lib/templates/createDevice.json index 07041c4a1..927e63ad6 100644 --- a/lib/templates/createDevice.json +++ b/lib/templates/createDevice.json @@ -36,6 +36,10 @@ "description": "Transport protocol used by the platform to communicate with the device", "type": "string" }, + "expressionLanguage": { + "description": "Expression language used to apply expressions for this device", + "type": "string" + }, "lazy": { "description": "list of lazy attributes of the devices", "type": "array", @@ -89,8 +93,7 @@ }, "expression": { "description": "Optional expression for measurement transformation", - "type": "string", - "pattern": "^([^<>;'=]+)+$" + "type": "string" }, "entity_name": { "description": "Optional entity name for multientity updates", diff --git a/lib/templates/deviceGroup.json b/lib/templates/deviceGroup.json index 0816012a6..8c9279cdb 100644 --- a/lib/templates/deviceGroup.json +++ b/lib/templates/deviceGroup.json @@ -37,6 +37,10 @@ "description": "list of lazy attributes of the devices", "type": "array" }, + "expressionLanguage": { + "description": "Expression language used to for the group of devices", + "type": "string" + }, "attributes": { "description": "list of active attributes of the devices", "type": "array" diff --git a/lib/templates/updateDevice.json b/lib/templates/updateDevice.json index c8e5f71e4..4a0acee76 100644 --- a/lib/templates/updateDevice.json +++ b/lib/templates/updateDevice.json @@ -81,8 +81,7 @@ }, "expression": { "description": "Optional expression for measurement transformation", - "type": "string", - "pattern": "^([^<>;'=]+)+$" + "type": "string" }, "entity_name": { "description": "Optional entity name for multientity updates", diff --git a/package.json b/package.json index 61dd653fd..dae453131 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "body-parser": "~1.19.0", "command-shell-lib": "1.0.0", "express": "~4.16.4", + "jexl": "2.1.1", "jison": "0.4.18", "logops": "2.1.0", "moment": "~2.24.0", diff --git a/test/unit/expressions/expressionCombinedTransformations-test.js b/test/unit/expressions/expressionCombinedTransformations-test.js new file mode 100644 index 000000000..8132d9474 --- /dev/null +++ b/test/unit/expressions/expressionCombinedTransformations-test.js @@ -0,0 +1,285 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../lib/fiware-iotagent-lib'), + utils = require('../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfigJexl = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'legacy', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }, + iotAgentConfigLegacy = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'jexl', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Combine Jexl and legacy expressions (default JEXL)', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigJexl, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy"', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL" - default', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); + +describe('Combine Jexl and legacy expressions (default Legacy)', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigLegacy, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy" - default', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL"', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); diff --git a/test/unit/expressions/jexlBasedTransformations-test.js b/test/unit/expressions/jexlBasedTransformations-test.js new file mode 100644 index 000000000..31f74cd0e --- /dev/null +++ b/test/unit/expressions/jexlBasedTransformations-test.js @@ -0,0 +1,305 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../lib/fiware-iotagent-lib'), + utils = require('../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'Light': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + }, + 'LightError': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * / 20' + } + ] + }, + 'WeatherStation': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity / 2) + " and pressure " + (pressure * 20)' + } + ] + }, + 'WeatherStationMultiple': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure25', + type: 'Hgmm', + expression: 'pressure * 20' + }, + { + object_id: 'h', + name: 'humidity12', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity12 / 2) + " and pressure " + (pressure25 * 20)' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Javascript Expression Language (JEXL) based transformations plugin ', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for attributes with expressions', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin1.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin1Success.json')); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for expressions with syntax errors', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin4.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin1Success.json')); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'LightError', '', values, function(error) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + error.code.should.equal(400); + done(); + }); + }); + }); + + describe('When there are expression attributes that are just calculated (not sent by the device)', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin2.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin2Success.json')); + }); + + it('should calculate them and add them to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an expression with multiple variables with numbers arrive', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin5.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin5Success.json')); + }); + + it('should calculate it and add it to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a measure arrives and there is not enough information to calculate an expression', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should not calculate the expression', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/expressions/jexlExpression-test.js b/test/unit/expressions/jexlExpression-test.js new file mode 100644 index 000000000..fd82c4de3 --- /dev/null +++ b/test/unit/expressions/jexlExpression-test.js @@ -0,0 +1,240 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + */ +'use strict'; + +var should = require('should'), + expressionParser = require('../../../lib/plugins/jexlParser'); + +describe('Jexl expression interpreter', function() { + var arithmetic, + scope = { + value: 6, + other: 3, + theString: '12.6, -19.4', + spaces: '5 a b c d 5', + big: 2000, + number: 145, + number2: 155, + number3inside: 200, + array: [1,2], + object: { + name: 'John', + surname: 'Doe' + } + }; + + describe('When a expression with a single value is parsed', function() { + it('should return that value', function(done) { + expressionParser.parse('5 * value', scope, function(error, result) { + should.not.exist(error); + result.should.equal(30); + done(); + }); + }); + }); + + arithmetic = [ + ['5 * value', 30], + ['(6 + value) * 3', 36], + ['value / 12 + 1', 1.5], + ['(5 + 2) * (value + 7)', 91], + ['(5 - other) * (value + 7)', 26], + ['3 * 5.2', 15.6], + ['value * 5.2', 31.2] + ]; + + function arithmeticUseCase(arithmeticExpr) { + describe('When an expression with the arithmetic operation [' + arithmeticExpr[0] + '] is parsed', function() { + it('should be interpreted appropriately', function(done) { + expressionParser.parse(arithmeticExpr[0], scope, function(error, result) { + should.not.exist(error); + result.should.approximately(arithmeticExpr[1], 0.000001); + done(); + }); + }); + }); + } + + for (var i = 0; i < arithmetic.length; i++) { + arithmeticUseCase(arithmetic[i]); + } + + describe('When an expression with two strings is concatenated', function() { + it('should return the concatenation of both strings', function(done) { + expressionParser.parse('"Pruebas" + "DeStrings"', scope, function(error, result) { + should.not.exist(error); + result.should.equal('PruebasDeStrings'); + done(); + }); + }); + }); + + describe('When string transformation functions are executed', function() { + it('should return the appropriate piece of the string', function(done) { + expressionParser.parse( + 'theString|substr(theString|indexOf(\",\") + 1, theString|length)|trim', + scope, + function(error, result) { + should.not.exist(error); + result.should.equal('-19.4'); + done(); + }); + }); + }); + + describe('When an expression contains variables with numbers', function() { + it('should return the appropriate result', function(done) { + expressionParser.parse('number + number2 + number3inside', + scope, function(error, result) { + should.not.exist(error); + result.should.equal(500); + done(); + }); + }); + }); + + describe('When an expression contains multiple parenthesis', function() { + it('should return the appropriate result', function(done) { + expressionParser.parse('((number) * (number2))', + scope, function(error, result) { + should.not.exist(error); + result.should.equal(22475); + done(); + }); + }); + }); + + describe('When trim() function is executed', function() { + it('should return the appropriate piece of the string', function(done) { + expressionParser.parse( + 'spaces|trim', + scope, + function(error, result) { + should.not.exist(error); + result.should.equal('5 a b c d 5'); + done(); + }); + }); + }); + + describe('When an expression with strings containing spaces is concatenated', function() { + it('should honour the whitespaces', function(done) { + expressionParser.parse('"Pruebas " + "De Strings"', scope, function(error, result) { + should.not.exist(error); + result.should.equal('Pruebas De Strings'); + done(); + }); + }); + }); + + describe('When an expression with strings with single quotation marks is parsed', function() { + it('should accept the strings', function(done) { + expressionParser.parse('\'Pruebas \' + \'De Strings\'', scope, function(error, result) { + should.not.exist(error); + result.should.equal('Pruebas De Strings'); + done(); + }); + }); + }); + + describe('When a string is concatenated with a number', function() { + it('should result in a string concatenation', function(done) { + expressionParser.parse('"number " + 5', scope, function(error, result) { + should.not.exist(error); + result.should.equal('number 5'); + done(); + }); + }); + }); + + describe('When an expression with a parse error is parsed', function() { + it('should raise an INVALID_EXPRESSION error', function(done) { + expressionParser.parse('"numb+sd ññ ((', scope, function(error, result) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + done(); + }); + }); + }); + + describe('When an string function is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('(24 * big)|indexOf("80")', scope, function(error, result) { + should.not.exist(error); + result.should.equal(1); + done(); + }); + }); + }); + + describe('When an ternary operator is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('value == 6? true : false', scope, function(error, result) { + should.not.exist(error); + result.should.equal(true); + done(); + }); + }); + }); + + describe('When an logic operator is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('value == 6 && spaces|indexOf("a")>0', scope, function(error, result) { + should.not.exist(error); + result.should.equal(true); + done(); + }); + }); + }); + + describe('When a function is applied to an array', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('array[1]+1', scope, function(error, result) { + should.not.exist(error); + result.should.equal(3); + done(); + }); + }); + }); + + describe('When a function is applied to an object', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('object.name', scope, function(error, result) { + should.not.exist(error); + result.should.equal('John'); + done(); + }); + }); + }); + + describe('When an expression aims at creating an object', function() { + it('it should work', function(done) { + expressionParser.parse('{type:"Point",coordinates: [value,other]}', scope, function(error, result) { + should.not.exist(error); + result.should.deepEqual({type:'Point',coordinates: [6,3]}); + done(); + }); + }); + }); + +}); diff --git a/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js b/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js new file mode 100644 index 000000000..212025326 --- /dev/null +++ b/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js @@ -0,0 +1,287 @@ +/* + * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../../lib/fiware-iotagent-lib'), + utils = require('../../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfigJexl = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'legacy', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }, + iotAgentConfigLegacy = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041 + }, + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'jexl', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Combine Jexl and legacy expressions (default JEXL) - NGSI v2', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigJexl, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy"', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL" - default', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws2/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws2', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); + +describe('Combine Jexl and legacy expressions (default Legacy) - NGSI v2', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigLegacy, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy" - default', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws3/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws3', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL"', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws4/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws4', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); diff --git a/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js new file mode 100644 index 000000000..aedf953e4 --- /dev/null +++ b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js @@ -0,0 +1,854 @@ +/* + * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../../lib/fiware-iotagent-lib'), + utils = require('../../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + defaultExpressionLanguage: 'jexl', + server: { + port: 4041 + }, + types: { + 'Light': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + }, + { + object_id: 'm', + name: 'manufacturer', + type: 'Object', + }, + { + object_id: 'r', + name: 'revisions', + type: 'Array', + }, + { + object_id: 'x', + name: 'consumption_x', + type: 'Number', + expression: 'pressure * 20' + } + ] + }, + 'LightError': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * / 20' + } + ] + }, + 'WeatherStation': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: 'consumption * 20' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity / 2) + " and pressure " + (pressure * 20)' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: 'alive * 20' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: 'updated * 20' + }, + ] + }, + 'WeatherStationMultiple': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure|trim' + }, + { + object_id: 'p25', + name: 'pressure25', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: 'consumption|trim' + }, + { + object_id: 'h', + name: 'humidity12', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity12 / 2) + " and pressure " + (pressure25 * 20)' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: 'alive|trim' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: 'updated|trim' + }, + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Java expression language (JEXL) based transformations plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for expressions with syntax errors', function() { + // Case: Update for an attribute with bad expression + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'LightError', '', values, function(error) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + error.code.should.equal(400); + done(); + }); + }); + }); + + describe('When there are expression attributes that are just calculated (not sent by the device)', function() { + // Case: Expression which results is sent as a new attribute + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin2.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should calculate them and add them to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an expression with multiple variables with numbers arrive', function() { + // Case: Update for integer and string attributes with expression + + var values = [ + { + name: 'p25', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin4.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should calculate it and add it to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type integer', function() { + // Case: Update for an integer attribute without expression + var values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin11.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type integer', function() { + // Case: Update for an integer attribute with arithmetic expression + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expression and type integer', function() { + // Case: Update for an integer attribute with string expression + var values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin11.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type float', function() { + // Case: Update for a Float attribute without expressions + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin3.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type float', function() { + // Case: Update for a Float attribute with arithmetic expression + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin8.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and type float', function() { + // Case: Update for a Float attribute with string expression + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin3.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and NULL type', function() { + // Case: Update for a Null attribute without expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and NULL type', function() { + // Case: Update for a Null attribute with arithmetic expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and NULL type', function() { + // Case: Update for a Null attribute with string expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Boolean type', function() { + // Case: Update for a Boolean attribute without expression + + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin9.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with arithmetic expression + + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin10.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with string expression + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin9.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON document attribute without expression + var values = [ + { + name: 'm', + type: 'Object', + value: { name: 'Manufacturer1', VAT: 'U12345678' } + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin6.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON array attribute without expression + + var values = [ + { + name: 'r', + type: 'Object', + value: ['v0.1', 'v0.2', 'v0.3'] + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin7.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are not updated', function() { + + var values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin12.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are updated', function() { + + var values = [ + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin13.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are updated' + + '(overriding situation)', function() { + + var values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + }, + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin13.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); From 042c9fd5e172798b856b6012206b2dddf8765454 Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Tue, 11 Feb 2020 14:55:39 +0100 Subject: [PATCH 2/4] fix tables using and | use code --- doc/expressionLanguage.md | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/doc/expressionLanguage.md b/doc/expressionLanguage.md index 8fc42798e..e68eec84f 100644 --- a/doc/expressionLanguage.md +++ b/doc/expressionLanguage.md @@ -353,36 +353,41 @@ For more details, check JEXL language details ### Examples of expressions -The following table shows expressions and their expected outcomes for a measure -with four attributes: "value" with value 6, "name" with value "DevId629", -an object with value {name: "John", surname: "Doe"} and array of value [1,3]. - -| Expression | Expected outcome | -|:--------------------------- |:--------------------- | -| '5 * value' | 30 | -| '(6 + value) * 3' | 36 | -| 'value / 12 + 1' | 1.5 | -| '(5 + 2) * (value + 7)' | 91 | -| 'value * 5.2' | 31.2 | -| '"Pruebas " + "De Strings"' | 'Pruebas De Strings' | -| 'name + "value is " +value' | 'DevId629 value is 6' | +The following table shows expressions and their expected outcomes taking into +account the following measures at southbound interface: + +* `value` with value 6 (number) +* `name` with value `"DevId629"` (string) +* `object` with value `{name: "John", surname: "Doe"}` (JSON object) +* `array` with value `[1, 3]` (JSON Array) + + +| Expression | Expected outcome | +|:--------------------------- |:----------------------- | +| `5 * value` | `30` | +| `(6 + value) * 3` | `36` | +| `value / 12 + 1` | `1.5` | +| `(5 + 2) * (value + 7)` | `91` | +| `value * 5.2` | `31.2` | +| `"Pruebas " + "De Strings"` | `"Pruebas De Strings"` | +| `name + "value is " +value` | `"DevId629 value is 6"` | Support for `trim`, `length`, `substr` and `indexOf` transformations was added. -| Expression | Expected outcome | -|:--------------------------- |:--------------------- | -| '" a "|trim' | 'a' | -| 'name|length' | 8 | -| 'name|indexOf("e")' | 1 | -| 'name|substring(0,name|indexOf("e")+1)'| 'De' | +| Expression | Expected outcome | +|:--------------------------- |:----------------------- | +| " a "|trim | `a` | +| name|length | `8` | +| name|indexOf("e")| `1` | +| name|substring(0,name|indexOf("e")+1)| `"De"` | The following are some expressions not supported by the legacy expression language: -| Expression | Expected outcome | -|:--------------------------- |:--------------------- | -| 'value == 6? true : false' | true | -| 'value == 6 && name|indexOf("e")>0| true | -| 'array[1]+1' | 3 | -| 'object.name' | 'John' | -| '{type:"Point",coordinates: [value,value]}' | {type:"Point",coordinates: [6,6]}| +| Expression | Expected outcome | +|:----------------------------------- |:----------------------- | +| `value == 6? true : false` | `true` | +| value == 6 && name|indexOf("e")>0 | `true` | +| `array[1]+1` | `3` | +| `object.name` | `"John"` | +| `{type:"Point",coordinates: [value,value]}`| `{type:"Point",coordinates: [6,6]}` | From 8eb5505d85d31d10277fee75062a9593452a9d0e Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Tue, 11 Feb 2020 16:49:45 +0100 Subject: [PATCH 3/4] apply fix suggested by @fgalan --- doc/installationguide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/installationguide.md b/doc/installationguide.md index f76f98102..5072e0942 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -211,7 +211,7 @@ used for the same purpose. For instance: [Cluster](https://nodejs.org/api/cluster.html) module in Node.js and [this section](howto.md#iot-agent-in-multi-thread-mode) of the library documentation. - **defaultExpressionLanguage**: the default expression language used to - compute expressions, possible values are: `legacy|jexl`. When not set or + compute expressions, possible values are: `legacy` or `jexl`. When not set or wrongly set, `legacy` is used as default value. From 3b6824ba8e3cb1760d32dbca44e2fa91de6ef03a Mon Sep 17 00:00:00 2001 From: "Federico M. Facca" Date: Tue, 11 Feb 2020 16:57:17 +0100 Subject: [PATCH 4/4] apply suggestion by @fgalan --- doc/expressionLanguage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/expressionLanguage.md b/doc/expressionLanguage.md index e68eec84f..394c53aad 100644 --- a/doc/expressionLanguage.md +++ b/doc/expressionLanguage.md @@ -306,7 +306,8 @@ two possible types of expressions: Integer (arithmetic operations) or Strings. As an alternative, the IoTAgent Library supports as well [JEXL](https://github.com/TomFrost/jexl). To use JEXL, you will need to either configure it as default -language using the `defaultExpressionLanguage` field to `jexl` or configuring +language using the `defaultExpressionLanguage` field to `jexl` +(see [configuration documentation](installationguide.md)) or configuring the usage of JEXL as expression language for a given device: ```