@machinemetrics/io-adapter-lib 2.32.3 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/lib/config/engineConfigV2.js +1 -1
- package/lib/config/transformConfigUtil.js +20 -9
- package/lib/engine/transformBuilderV2.js +5 -0
- package/lib/expressionService.js +28 -3
- package/lib/transform/expression.js +23 -1
- package/lib/transform/map.js +12 -2
- package/lib/transform/source.js +11 -2
- package/lib/transform/transformState.js +3 -0
- package/package.json +1 -1
- package/test/configFiles/transform/map.yml +11 -0
- package/test/configFiles/transform/source.yml +12 -0
- package/test/expressionService.test.js +36 -0
- package/test/transform/map.test.js +33 -0
- package/test/transform/source.test.js +14 -0
- package/test/transform/state.test.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.34.0]
|
|
4
|
+
- Added support for initializing variables from constant source or expression ops
|
|
5
|
+
- Fixed map not recognizing external identifiers in sub-ops
|
|
6
|
+
- Allow string and bool constants in shorthand variable declarations
|
|
7
|
+
|
|
8
|
+
## [2.33.0]
|
|
9
|
+
- Added support for numeric indexing in expressions
|
|
10
|
+
- Fixed map not allowing any transforms to be chained beyond it
|
|
11
|
+
|
|
3
12
|
## [2.32.3]
|
|
4
13
|
- Added opcua-conditions options for code format and allow/deny nodes
|
|
5
14
|
|
|
@@ -42,7 +42,7 @@ class EngineConfig {
|
|
|
42
42
|
// - source: composite.parts
|
|
43
43
|
|
|
44
44
|
this.config.variables = _.mapValues(this.config.variables, (variable) => {
|
|
45
|
-
if (_.isString(variable)) {
|
|
45
|
+
if (_.isString(variable) || _.isNumber(variable) || _.isBoolean(variable)) {
|
|
46
46
|
return [{ source: variable }];
|
|
47
47
|
}
|
|
48
48
|
return variable;
|
|
@@ -94,18 +94,22 @@ class TransformConfigUtil {
|
|
|
94
94
|
|
|
95
95
|
getExpressionsFromVariables(variables) {
|
|
96
96
|
return _(variables).map((defn, varName) => {
|
|
97
|
-
return
|
|
98
|
-
return _.map(transformContainer, (body, attribute) => {
|
|
99
|
-
if (this.transformMap[attribute]) {
|
|
100
|
-
return this.transformMap[attribute].getExpressions(this, body, { varName, index, attribute });
|
|
101
|
-
}
|
|
102
|
-
return [];
|
|
103
|
-
});
|
|
104
|
-
});
|
|
97
|
+
return this.getExpressionsFromChain(defn, varName);
|
|
105
98
|
}).flattenDeep().compact().value();
|
|
106
99
|
}
|
|
107
100
|
|
|
108
|
-
|
|
101
|
+
getExpressionsFromChain(chain, varName, ancestorVars = []) {
|
|
102
|
+
return _.map(chain, (transformContainer, index) => {
|
|
103
|
+
return _.map(transformContainer, (body, attribute) => {
|
|
104
|
+
if (this.transformMap[attribute]) {
|
|
105
|
+
return this.transformMap[attribute].getExpressions(this, body, { varName, index, attribute, ancestorVars });
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getExpressions(body, { varName, index, attribute, field, ancestorVars }, includeDefault = false) {
|
|
109
113
|
const parts = [];
|
|
110
114
|
|
|
111
115
|
let expression = '';
|
|
@@ -140,6 +144,13 @@ class TransformConfigUtil {
|
|
|
140
144
|
expression,
|
|
141
145
|
});
|
|
142
146
|
|
|
147
|
+
_.each(ancestorVars, (ancestorVar) => {
|
|
148
|
+
parts.push({
|
|
149
|
+
path: `variables.${ancestorVar}`,
|
|
150
|
+
expression,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
143
154
|
return parts;
|
|
144
155
|
}
|
|
145
156
|
|
|
@@ -34,6 +34,11 @@ class TransformBuilderV2 {
|
|
|
34
34
|
engine.addVariable(varName, transform);
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
_.each(engine.variablePool, (chain, varName) => {
|
|
39
|
+
const context = { sourceType: 'init', trigger: varName };
|
|
40
|
+
chain.init(context, 0);
|
|
41
|
+
});
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
createTransformChain(engine, transformList, varName, base = false) {
|
package/lib/expressionService.js
CHANGED
|
@@ -9,6 +9,7 @@ class ExpressionService {
|
|
|
9
9
|
this.availableNamesByChannel = {};
|
|
10
10
|
this.referencedNamesByChannel = {};
|
|
11
11
|
this.referencedNamesByPath = {};
|
|
12
|
+
this.selfReferencedPaths = {};
|
|
12
13
|
this.pathsByReferenceName = {};
|
|
13
14
|
this.defaultValues = {};
|
|
14
15
|
this.channelsLookup = {};
|
|
@@ -117,6 +118,10 @@ class ExpressionService {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
});
|
|
121
|
+
|
|
122
|
+
if (this.findName(expression, 'this')) {
|
|
123
|
+
this.selfReferencedPaths[path] = true;
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
addStringExpression({ string, path, withThis } = {}) {
|
|
@@ -196,7 +201,7 @@ class ExpressionService {
|
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
findCompoundIdentifiers(str) {
|
|
199
|
-
const matches = str.match(/[A-Za-z_][A-Za-z0-9_-]*(?:(?:\.[A-Za-z][A-Za-z0-9_-]*)|(?:\['[^'"]+'\]))+/g);
|
|
204
|
+
const matches = str.match(/[A-Za-z_][A-Za-z0-9_-]*(?:(?:\.[A-Za-z][A-Za-z0-9_-]*)|(?:\[('[^'"]+'|\d+)\]))+/g);
|
|
200
205
|
return matches ?? [];
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -216,7 +221,13 @@ class ExpressionService {
|
|
|
216
221
|
rest.push(match[1]);
|
|
217
222
|
compound = compound.substr(match[0].length);
|
|
218
223
|
} else {
|
|
219
|
-
|
|
224
|
+
match = compound.match(/^\[(\d+)\]/);
|
|
225
|
+
if (match) {
|
|
226
|
+
rest.push(+match[1]);
|
|
227
|
+
compound = compound.substr(match[0].length);
|
|
228
|
+
} else {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
233
|
}
|
|
@@ -249,6 +260,12 @@ class ExpressionService {
|
|
|
249
260
|
const first = tokens[0];
|
|
250
261
|
if (first === 'this' || this.channelsLookup[first]) {
|
|
251
262
|
accum[first] = _.reduceRight(_.tail(tokens), (sub, item) => {
|
|
263
|
+
if (_.isNumber(item)) {
|
|
264
|
+
return [
|
|
265
|
+
..._.times(Math.max(0, item - 1), () => 0),
|
|
266
|
+
sub,
|
|
267
|
+
];
|
|
268
|
+
}
|
|
252
269
|
return { [item]: sub };
|
|
253
270
|
}, 0);
|
|
254
271
|
} else {
|
|
@@ -261,7 +278,11 @@ class ExpressionService {
|
|
|
261
278
|
try {
|
|
262
279
|
return this.compileInnerExpression(expression, { ...scope, ...addedScope });
|
|
263
280
|
} catch (err) {
|
|
264
|
-
|
|
281
|
+
let message = err.message;
|
|
282
|
+
if (message.startsWith('Index out')) {
|
|
283
|
+
message = `${message}. Indexes in expressions start at 1`;
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`Problem evaluating expression: ${message}`);
|
|
265
286
|
}
|
|
266
287
|
}
|
|
267
288
|
|
|
@@ -282,6 +303,10 @@ class ExpressionService {
|
|
|
282
303
|
return this.referencedNamesByPath[path] || [];
|
|
283
304
|
}
|
|
284
305
|
|
|
306
|
+
expressionHasSelfReference(path) {
|
|
307
|
+
return !!this.selfReferencedPaths[path];
|
|
308
|
+
}
|
|
309
|
+
|
|
285
310
|
expressionsTriggeredBy(name) {
|
|
286
311
|
return this.pathsByReferenceName[name] || [];
|
|
287
312
|
}
|
|
@@ -30,6 +30,18 @@ class ExpressionFilter extends TransformState {
|
|
|
30
30
|
this.engine = engine;
|
|
31
31
|
this.compiledExpression = compiledExpression;
|
|
32
32
|
this.selfSources = engine.expressionService.expressionTriggers(path);
|
|
33
|
+
this.hasSelfReference = engine.expressionService.expressionHasSelfReference(path);
|
|
34
|
+
|
|
35
|
+
// If there's no variables in the expression, it's a constant that can be evaluated once
|
|
36
|
+
if (_.isEmpty(this.selfSources) && !this.hasSelfReference) {
|
|
37
|
+
try {
|
|
38
|
+
this.constantSource = true;
|
|
39
|
+
this.constantValue = this.compiledExpression.evaluate();
|
|
40
|
+
this.lastSuppliedValue = this.constantValue;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Oh well
|
|
43
|
+
}
|
|
44
|
+
}
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
static op = 'expression';
|
|
@@ -49,6 +61,12 @@ class ExpressionFilter extends TransformState {
|
|
|
49
61
|
return configUtil.getExpressions(body, info);
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
init(context, time) {
|
|
65
|
+
if (this.constantSource) {
|
|
66
|
+
this.update(context, this.lastSuppliedValue, time);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
update(context, value, time) {
|
|
53
71
|
if (value === 'UNAVAILABLE') {
|
|
54
72
|
this.setUnavailable(context, time);
|
|
@@ -58,7 +76,11 @@ class ExpressionFilter extends TransformState {
|
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
filter(context, value, time) {
|
|
61
|
-
if (this.constantSource
|
|
79
|
+
if (this.constantSource) {
|
|
80
|
+
this.commitValue(context, this.constantValue, time);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.simpleExpression) {
|
|
62
84
|
this.commitValue(context, value, time);
|
|
63
85
|
return;
|
|
64
86
|
}
|
package/lib/transform/map.js
CHANGED
|
@@ -47,7 +47,7 @@ class MapFilter extends TransformState {
|
|
|
47
47
|
constructor({ engine, /* path, rootPath, baseTransform, */ args: { transforms } }, builder) {
|
|
48
48
|
super();
|
|
49
49
|
|
|
50
|
-
this.
|
|
50
|
+
this.innerChain = builder.createTransformChain(engine, transforms);
|
|
51
51
|
|
|
52
52
|
this.makeChain = () => {
|
|
53
53
|
return new MapChain(builder.createTransformChain(engine, transforms));
|
|
@@ -72,13 +72,23 @@ class MapFilter extends TransformState {
|
|
|
72
72
|
defn.args = { transforms };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
static getExpressions(configUtil, body, info) {
|
|
76
|
+
const varName = `${info.varName}.${info.index}.${info.attribute}`;
|
|
77
|
+
const ancestorVars = [
|
|
78
|
+
...info.ancestorVars,
|
|
79
|
+
info.varName,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return configUtil.getExpressionsFromChain(body, varName, ancestorVars);
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
initReset() {
|
|
76
86
|
this.chainList = [];
|
|
77
87
|
this.value = [];
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
supportsPendingChanges() {
|
|
81
|
-
return this.
|
|
91
|
+
return this.innerChain.supportsPendingChanges();
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
updateChainList(size) {
|
package/lib/transform/source.js
CHANGED
|
@@ -40,8 +40,9 @@ class SourceFilter extends TransformState {
|
|
|
40
40
|
// If there's no variables in the expression, it's a constant that can be evaluated once
|
|
41
41
|
if (_.isEmpty(this.selfSources)) {
|
|
42
42
|
try {
|
|
43
|
-
this.lastSuppliedValue = this.compiledExpression.evaluate();
|
|
44
43
|
this.constantSource = true;
|
|
44
|
+
this.constantValue = this.compiledExpression.evaluate();
|
|
45
|
+
this.lastSuppliedValue = this.constantValue;
|
|
45
46
|
} catch (err) {
|
|
46
47
|
// Oh well
|
|
47
48
|
}
|
|
@@ -77,6 +78,12 @@ class SourceFilter extends TransformState {
|
|
|
77
78
|
return configUtil.getExpressions(body, info);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
init(context, time) {
|
|
82
|
+
if (this.constantSource) {
|
|
83
|
+
this.update(context, this.lastSuppliedValue, time);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
update(context, value, time) {
|
|
81
88
|
if (value === 'UNAVAILABLE') {
|
|
82
89
|
this.setUnavailable(context, time);
|
|
@@ -86,7 +93,9 @@ class SourceFilter extends TransformState {
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
filter(context, value, time) {
|
|
89
|
-
if (this.constantSource
|
|
96
|
+
if (this.constantSource) {
|
|
97
|
+
this.commitValue(context, this.constantValue, time);
|
|
98
|
+
} else if (this.simpleSource) {
|
|
90
99
|
this.commitValue(context, value, time);
|
|
91
100
|
} else {
|
|
92
101
|
const localState = {};
|
|
@@ -56,6 +56,9 @@ class TransformState extends EventEmitter {
|
|
|
56
56
|
return this.pendingChangeTime;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Called after all varaibles have been added to the engine.
|
|
60
|
+
init(_context, _time) { }
|
|
61
|
+
|
|
59
62
|
update(context, value, time) {
|
|
60
63
|
if (!this.available) {
|
|
61
64
|
this.available = true;
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ declare-keys:
|
|
|
7
7
|
- counter
|
|
8
8
|
- program
|
|
9
9
|
- complex
|
|
10
|
+
- complex2
|
|
11
|
+
- complex3
|
|
10
12
|
variables:
|
|
11
13
|
var1:
|
|
12
14
|
- source: counter
|
|
@@ -17,3 +19,12 @@ variables:
|
|
|
17
19
|
- source: complex
|
|
18
20
|
- map:
|
|
19
21
|
- expression: this.subkey
|
|
22
|
+
var3:
|
|
23
|
+
- source: complex2
|
|
24
|
+
- map:
|
|
25
|
+
- expression: this.subkey
|
|
26
|
+
- expression: this[1]
|
|
27
|
+
var4:
|
|
28
|
+
- source: complex3
|
|
29
|
+
- map:
|
|
30
|
+
- expression: this.subkey + counter
|
|
@@ -18,3 +18,15 @@ variables:
|
|
|
18
18
|
- expression: this + yact
|
|
19
19
|
var5:
|
|
20
20
|
- source: execution != 'MANUAL'
|
|
21
|
+
const1:
|
|
22
|
+
- source: 10
|
|
23
|
+
const2:
|
|
24
|
+
- source: true
|
|
25
|
+
const3:
|
|
26
|
+
- source: "'ACTIVE'"
|
|
27
|
+
const4:
|
|
28
|
+
- source: max(4, 5, 6)
|
|
29
|
+
var6:
|
|
30
|
+
- source: const1 + (const2 ? 20:30)
|
|
31
|
+
var7:
|
|
32
|
+
- source: xact + const1
|
|
@@ -80,6 +80,42 @@ describe('ExpressionService tests', function () {
|
|
|
80
80
|
expect(() => exprsvc.compileExpression('this + 1')).to.throw('Undefined symbol this');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
it('parses expressions with compound identifiers', function () {
|
|
84
|
+
const exprsvc = new ExpressionService();
|
|
85
|
+
|
|
86
|
+
const context1 = { this: 5 };
|
|
87
|
+
const expr1 = exprsvc.compileExpression('this', context1);
|
|
88
|
+
expect(expr1.evaluate(context1)).to.eq(5);
|
|
89
|
+
|
|
90
|
+
const context2 = { this: { subkey: 5 } };
|
|
91
|
+
const expr2 = exprsvc.compileExpression('this.subkey', context2);
|
|
92
|
+
expect(expr2.evaluate(context2)).to.eq(5);
|
|
93
|
+
|
|
94
|
+
const context3 = { this: { subkey: { secondary: 5 } } };
|
|
95
|
+
const expr3 = exprsvc.compileExpression('this.subkey.secondary', context3);
|
|
96
|
+
expect(expr3.evaluate(context3)).to.eq(5);
|
|
97
|
+
|
|
98
|
+
const context4 = { this: { subkey: { secondary: 5 } } };
|
|
99
|
+
const expr4 = exprsvc.compileExpression('this.subkey[\'secondary\']', context4);
|
|
100
|
+
expect(expr4.evaluate(context4)).to.eq(5);
|
|
101
|
+
|
|
102
|
+
const context5 = { this: { subkey: [0, 0, 5] } };
|
|
103
|
+
const expr5 = exprsvc.compileExpression('this.subkey[3]', context5);
|
|
104
|
+
expect(expr5.evaluate(context5)).to.eq(5);
|
|
105
|
+
|
|
106
|
+
const context6 = { this: [0, 0, 5] };
|
|
107
|
+
const expr6 = exprsvc.compileExpression('this[3]', context6);
|
|
108
|
+
expect(expr6.evaluate(context6)).to.eq(5);
|
|
109
|
+
|
|
110
|
+
const context7 = { this: { subkey: [0, 0, { bar: 5 }] } };
|
|
111
|
+
const expr7 = exprsvc.compileExpression('this.subkey[3].bar', context7);
|
|
112
|
+
expect(expr7.evaluate(context7)).to.eq(5);
|
|
113
|
+
|
|
114
|
+
const context8 = { this: { subkey: [0, 0, { bar: [0, { baz: 5 }] }] } };
|
|
115
|
+
const expr8 = exprsvc.compileExpression('this.subkey[3].bar[2].baz', context8);
|
|
116
|
+
expect(expr8.evaluate(context8)).to.eq(5);
|
|
117
|
+
});
|
|
118
|
+
|
|
83
119
|
/* eslint-disable no-template-curly-in-string */
|
|
84
120
|
|
|
85
121
|
it('parses string expressions', function () {
|
|
@@ -39,4 +39,37 @@ describe('map full engine config file tests', function () {
|
|
|
39
39
|
[[10, 20], 0],
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it('transform chained after map', function () {
|
|
44
|
+
const engine = new EngineV2(config);
|
|
45
|
+
const builder = new Builder(config);
|
|
46
|
+
builder.build(engine);
|
|
47
|
+
|
|
48
|
+
const source = testUtils.valueSource();
|
|
49
|
+
testUtils.attachEngineTransformValidator(engine, engine.variablePool.var3, source);
|
|
50
|
+
|
|
51
|
+
source.sendValue('complex2', [{ subkey: 10, xx: 15 }, { subkey: 20, xx: 30 }], 0);
|
|
52
|
+
|
|
53
|
+
engine.validateFilter([
|
|
54
|
+
[10, 0],
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('subkey map with external ref', function () {
|
|
59
|
+
const engine = new EngineV2(config);
|
|
60
|
+
const builder = new Builder(config);
|
|
61
|
+
builder.build(engine);
|
|
62
|
+
|
|
63
|
+
const source = testUtils.valueSource();
|
|
64
|
+
testUtils.attachEngineTransformValidator(engine, engine.variablePool.var4, source);
|
|
65
|
+
|
|
66
|
+
source.sendValue('counter', 100, 0);
|
|
67
|
+
source.sendValue('complex3', [{ subkey: 10, xx: 15 }, { subkey: 20, xx: 30 }], 0);
|
|
68
|
+
source.sendValue('counter', 200, 2);
|
|
69
|
+
|
|
70
|
+
engine.validateFilter([
|
|
71
|
+
[[110, 120], 0],
|
|
72
|
+
[[210, 220], 2],
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
42
75
|
});
|
|
@@ -128,6 +128,20 @@ describe('source full engine config file tests', async function () {
|
|
|
128
128
|
[true, 0],
|
|
129
129
|
]);
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
it('calculates constant values correctly', async function () {
|
|
133
|
+
const engine = new EngineV2(config);
|
|
134
|
+
const builder = new Builder(config);
|
|
135
|
+
builder.build(engine);
|
|
136
|
+
|
|
137
|
+
const source = new ValueSource();
|
|
138
|
+
engine.addValueSource(source);
|
|
139
|
+
|
|
140
|
+
expect(engine.getState('const1').value).to.eq(10);
|
|
141
|
+
expect(engine.getState('const2').value).to.eq(true);
|
|
142
|
+
expect(engine.getState('const3').value).to.eq('ACTIVE');
|
|
143
|
+
expect(engine.getState('const4').value).to.eq(6);
|
|
144
|
+
});
|
|
131
145
|
});
|
|
132
146
|
|
|
133
147
|
class ValueSource extends EventEmitter {
|
|
@@ -136,6 +136,9 @@ describe('expression transform tests', function () {
|
|
|
136
136
|
const source = testUtils.valueSource();
|
|
137
137
|
testUtils.attachEngineTransformValidator(engine, engine.variablePool.test5, source);
|
|
138
138
|
|
|
139
|
+
// Set by const init
|
|
140
|
+
expect(engine.getState('test5').value).to.eq('READY');
|
|
141
|
+
|
|
139
142
|
source.sendValue('exec', 'ACTIVE', 0);
|
|
140
143
|
source.sendValue('in-fault', false, 2);
|
|
141
144
|
source.sendValue('in-fault', false, 4);
|
|
@@ -144,7 +147,7 @@ describe('expression transform tests', function () {
|
|
|
144
147
|
source.sendValue('in-fault', false, 10);
|
|
145
148
|
|
|
146
149
|
engine.validateFilter([
|
|
147
|
-
['READY', 0],
|
|
150
|
+
// ['READY', 0], // Initial value doesn't come through because chain starts with constant
|
|
148
151
|
['ACTIVE', 2],
|
|
149
152
|
['ACTIVE', 4],
|
|
150
153
|
['STOPPED', 6],
|