@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 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 _.map(defn, (transformContainer, index) => {
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
- getExpressions(body, { varName, index, attribute, field }, includeDefault = false) {
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) {
@@ -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
- break;
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
- throw new Error(`Problem evaluating expression: ${errMessage}`);
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 || this.simpleSource) {
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
  }
@@ -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.chain = builder.createTransformChain(engine, transforms);
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.chain.supportsPendingChanges();
91
+ return this.innerChain.supportsPendingChanges();
82
92
  }
83
93
 
84
94
  updateChainList(size) {
@@ -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 || this.simpleSource) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machinemetrics/io-adapter-lib",
3
- "version": "2.32.3",
3
+ "version": "2.34.0",
4
4
  "description": "Configuration and engine implementation for MachineMetrics AdapterScripts and adapters",
5
5
  "main": "index.js",
6
6
  "license": "UNLICENSED",
@@ -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],