@fincity/kirun-js 2.16.2 → 3.0.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/__tests__/engine/runtime/expression/ExpressionEvaluatorLengthSubtractionBugTest.ts +384 -0
- package/__tests__/engine/runtime/expression/ExpressionEvaluatorLengthSubtractionTest.ts +338 -0
- package/__tests__/engine/runtime/expression/ExpressionTest.ts +7 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +56 -39
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/engine/function/system/context/SetFunction.ts +226 -75
- package/src/engine/runtime/expression/Expression.ts +267 -71
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +317 -22
- package/src/engine/runtime/expression/ExpressionLexer.ts +365 -0
- package/src/engine/runtime/expression/ExpressionParser.ts +541 -0
- package/src/engine/runtime/expression/ExpressionParserDebug.ts +21 -0
- package/src/engine/runtime/expression/Operation.ts +1 -0
- package/src/engine/runtime/expression/tokenextractor/ObjectValueSetterExtractor.ts +134 -31
- package/src/engine/runtime/expression/tokenextractor/TokenValueExtractor.ts +32 -8
- package/src/engine/util/LinkedList.ts +1 -1
|
@@ -10,11 +10,8 @@ import { FunctionSignature } from '../../../model/FunctionSignature';
|
|
|
10
10
|
import { Parameter } from '../../../model/Parameter';
|
|
11
11
|
import { Namespaces } from '../../../namespaces/Namespaces';
|
|
12
12
|
import { ContextElement } from '../../../runtime/ContextElement';
|
|
13
|
-
import { Expression } from '../../../runtime/expression/Expression';
|
|
14
13
|
import { ExpressionEvaluator } from '../../../runtime/expression/ExpressionEvaluator';
|
|
15
|
-
import {
|
|
16
|
-
import { ExpressionTokenValue } from '../../../runtime/expression/ExpressionTokenValue';
|
|
17
|
-
import { Operation } from '../../../runtime/expression/Operation';
|
|
14
|
+
import { TokenValueExtractor } from '../../../runtime/expression/tokenextractor/TokenValueExtractor';
|
|
18
15
|
import { FunctionExecutionParameters } from '../../../runtime/FunctionExecutionParameters';
|
|
19
16
|
import { isNullValue } from '../../../util/NullCheck';
|
|
20
17
|
import { StringFormatter } from '../../../util/string/StringFormatter';
|
|
@@ -56,110 +53,264 @@ export class SetFunction extends AbstractFunction {
|
|
|
56
53
|
|
|
57
54
|
let value: any = context?.getArguments()?.get(VALUE);
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
!contextToken.getExpression().startsWith('Context') ||
|
|
65
|
-
contextToken instanceof Expression ||
|
|
66
|
-
(contextToken instanceof ExpressionTokenValue &&
|
|
67
|
-
!(contextToken as ExpressionTokenValue)
|
|
68
|
-
.getElement()
|
|
69
|
-
.toString()
|
|
70
|
-
.startsWith('Context'))
|
|
71
|
-
) {
|
|
56
|
+
// Use TokenValueExtractor.splitPath for consistent path parsing
|
|
57
|
+
const parts = TokenValueExtractor.splitPath(key);
|
|
58
|
+
|
|
59
|
+
if (parts.length < 1 || parts[0] !== 'Context') {
|
|
72
60
|
throw new ExecutionException(
|
|
73
61
|
StringFormatter.format('The context path $ is not a valid path in context', key),
|
|
74
62
|
);
|
|
75
63
|
}
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
// Evaluate any dynamic expressions in the path (e.g., Context.a[Steps.loop.index])
|
|
66
|
+
const evaluatedParts = this.evaluateDynamicParts(parts, context);
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
68
|
+
return this.modifyContextWithParts(context, key, value, evaluatedParts);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Evaluate any dynamic expressions in path parts
|
|
73
|
+
* E.g., "Context.a[Steps.loop.index]" where the index is dynamic
|
|
74
|
+
*/
|
|
75
|
+
private evaluateDynamicParts(parts: string[], context: FunctionExecutionParameters): string[] {
|
|
76
|
+
const result: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
// Check if this part contains dynamic bracket expressions
|
|
80
|
+
const evaluated = this.evaluateBracketExpressions(part, context);
|
|
81
|
+
result.push(evaluated);
|
|
86
82
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Evaluate bracket expressions in a path part
|
|
89
|
+
* E.g., "arr[Steps.loop.index]" -> "arr[0]" if Steps.loop.index evaluates to 0
|
|
90
|
+
*/
|
|
91
|
+
private evaluateBracketExpressions(part: string, context: FunctionExecutionParameters): string {
|
|
92
|
+
// Find bracket expressions that need evaluation
|
|
93
|
+
let result = '';
|
|
94
|
+
let i = 0;
|
|
95
|
+
|
|
96
|
+
while (i < part.length) {
|
|
97
|
+
if (part[i] === '[') {
|
|
98
|
+
result += '[';
|
|
99
|
+
i++;
|
|
100
|
+
|
|
101
|
+
// Find the matching ]
|
|
102
|
+
let bracketContent = '';
|
|
103
|
+
let depth = 1;
|
|
104
|
+
let inQuote = false;
|
|
105
|
+
let quoteChar = '';
|
|
106
|
+
|
|
107
|
+
while (i < part.length && depth > 0) {
|
|
108
|
+
const ch = part[i];
|
|
109
|
+
|
|
110
|
+
if (inQuote) {
|
|
111
|
+
if (ch === quoteChar && part[i - 1] !== '\\') {
|
|
112
|
+
inQuote = false;
|
|
113
|
+
}
|
|
114
|
+
bracketContent += ch;
|
|
115
|
+
} else {
|
|
116
|
+
if (ch === '"' || ch === "'") {
|
|
117
|
+
inQuote = true;
|
|
118
|
+
quoteChar = ch;
|
|
119
|
+
bracketContent += ch;
|
|
120
|
+
} else if (ch === '[') {
|
|
121
|
+
depth++;
|
|
122
|
+
bracketContent += ch;
|
|
123
|
+
} else if (ch === ']') {
|
|
124
|
+
depth--;
|
|
125
|
+
if (depth > 0) bracketContent += ch;
|
|
126
|
+
} else {
|
|
127
|
+
bracketContent += ch;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if bracket content is a static value (number or quoted string)
|
|
134
|
+
if (/^-?\d+$/.test(bracketContent) ||
|
|
135
|
+
(bracketContent.startsWith('"') && bracketContent.endsWith('"')) ||
|
|
136
|
+
(bracketContent.startsWith("'") && bracketContent.endsWith("'"))) {
|
|
137
|
+
result += bracketContent + ']';
|
|
138
|
+
} else {
|
|
139
|
+
// Dynamic expression - evaluate it
|
|
140
|
+
try {
|
|
141
|
+
const evaluator = new ExpressionEvaluator(bracketContent);
|
|
142
|
+
const evaluatedValue = evaluator.evaluate(context.getValuesMap());
|
|
143
|
+
result += String(evaluatedValue) + ']';
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// If evaluation fails, keep original
|
|
146
|
+
result += bracketContent + ']';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
result += part[i];
|
|
151
|
+
i++;
|
|
152
|
+
}
|
|
98
153
|
}
|
|
99
|
-
|
|
154
|
+
|
|
155
|
+
return result;
|
|
100
156
|
}
|
|
101
157
|
|
|
102
|
-
private
|
|
158
|
+
private modifyContextWithParts(
|
|
103
159
|
context: FunctionExecutionParameters,
|
|
104
160
|
key: string,
|
|
105
161
|
value: any,
|
|
106
|
-
|
|
162
|
+
parts: string[],
|
|
107
163
|
): FunctionOutput {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
// parts[0] is "Context", parts[1] is the context element name
|
|
165
|
+
if (parts.length < 2) {
|
|
166
|
+
throw new KIRuntimeException(
|
|
167
|
+
StringFormatter.format("Context path '$' is too short", key),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get the first segment after "Context" - this should be a context element key
|
|
172
|
+
// The segment may contain bracket notation like "a[0]" which we need to parse
|
|
173
|
+
const firstSegment = parts[1];
|
|
174
|
+
const firstSegmentParts = this.parseBracketSegments(firstSegment);
|
|
175
|
+
const contextKey = firstSegmentParts[0];
|
|
176
|
+
|
|
177
|
+
let ce: ContextElement | undefined = context.getContext()?.get(contextKey);
|
|
115
178
|
|
|
116
179
|
if (isNullValue(ce)) {
|
|
117
180
|
throw new KIRuntimeException(
|
|
118
|
-
StringFormatter.format("Context doesn't have any element with name '$' ",
|
|
181
|
+
StringFormatter.format("Context doesn't have any element with name '$' ", contextKey),
|
|
119
182
|
);
|
|
120
183
|
}
|
|
121
184
|
|
|
122
|
-
|
|
185
|
+
// If we just have "Context.a" with no further path
|
|
186
|
+
if (parts.length === 2 && firstSegmentParts.length === 1) {
|
|
123
187
|
ce!.setElement(value);
|
|
124
188
|
return new FunctionOutput([EventResult.outputOf(new Map())]);
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
let el: any = ce!.getElement();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let token = tokens.removeLast();
|
|
131
|
-
let mem =
|
|
132
|
-
token instanceof ExpressionTokenValue
|
|
133
|
-
? (token as ExpressionTokenValue).getElement()
|
|
134
|
-
: token.getExpression();
|
|
135
|
-
|
|
192
|
+
|
|
193
|
+
// Initialize element if null
|
|
136
194
|
if (isNullValue(el)) {
|
|
137
|
-
|
|
195
|
+
// Determine if first access is array or object
|
|
196
|
+
const nextIsArray = firstSegmentParts.length > 1
|
|
197
|
+
? this.isArrayIndex(firstSegmentParts[1])
|
|
198
|
+
: (parts.length > 2 ? this.isArrayAccess(parts[2]) : false);
|
|
199
|
+
el = nextIsArray ? [] : {};
|
|
138
200
|
ce!.setElement(el);
|
|
139
201
|
}
|
|
140
202
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
203
|
+
// Collect all path segments (including bracket notation within segments)
|
|
204
|
+
const allSegments: { value: string; isArray: boolean }[] = [];
|
|
205
|
+
|
|
206
|
+
// Process remaining parts of the first segment (after context key)
|
|
207
|
+
for (let j = 1; j < firstSegmentParts.length; j++) {
|
|
208
|
+
allSegments.push({
|
|
209
|
+
value: this.stripQuotes(firstSegmentParts[j]),
|
|
210
|
+
isArray: this.isArrayIndex(firstSegmentParts[j])
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Process remaining parts (parts[2], parts[3], etc.)
|
|
215
|
+
for (let i = 2; i < parts.length; i++) {
|
|
216
|
+
const segmentParts = this.parseBracketSegments(parts[i]);
|
|
217
|
+
for (const seg of segmentParts) {
|
|
218
|
+
allSegments.push({
|
|
219
|
+
value: this.stripQuotes(seg),
|
|
220
|
+
isArray: this.isArrayIndex(seg)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Navigate to the parent of the final element
|
|
226
|
+
for (let i = 0; i < allSegments.length - 1; i++) {
|
|
227
|
+
const segment = allSegments[i];
|
|
228
|
+
const nextSegment = allSegments[i + 1];
|
|
229
|
+
|
|
230
|
+
if (segment.isArray) {
|
|
231
|
+
el = this.getDataFromArray(el, segment.value, nextSegment.isArray);
|
|
144
232
|
} else {
|
|
145
|
-
el = this.
|
|
233
|
+
el = this.getDataFromObject(el, segment.value, nextSegment.isArray);
|
|
146
234
|
}
|
|
147
|
-
|
|
148
|
-
op = ops.removeLast();
|
|
149
|
-
token = tokens.removeLast();
|
|
150
|
-
mem =
|
|
151
|
-
token instanceof ExpressionTokenValue
|
|
152
|
-
? (token as ExpressionTokenValue).getElement()
|
|
153
|
-
: token.getExpression();
|
|
154
235
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
236
|
+
|
|
237
|
+
// Set the final value
|
|
238
|
+
const lastSegment = allSegments[allSegments.length - 1];
|
|
239
|
+
if (lastSegment.isArray) {
|
|
240
|
+
this.putDataInArray(el, lastSegment.value, value);
|
|
241
|
+
} else {
|
|
242
|
+
this.putDataInObject(el, lastSegment.value, value);
|
|
243
|
+
}
|
|
158
244
|
|
|
159
245
|
return new FunctionOutput([EventResult.outputOf(new Map())]);
|
|
160
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Parse bracket segments from a path part
|
|
250
|
+
* E.g., "arr[0]" -> ["arr", "0"], "obj" -> ["obj"]
|
|
251
|
+
*/
|
|
252
|
+
private parseBracketSegments(part: string): string[] {
|
|
253
|
+
const segments: string[] = [];
|
|
254
|
+
let start = 0;
|
|
255
|
+
let i = 0;
|
|
256
|
+
|
|
257
|
+
while (i < part.length) {
|
|
258
|
+
if (part[i] === '[') {
|
|
259
|
+
if (i > start) {
|
|
260
|
+
segments.push(part.substring(start, i));
|
|
261
|
+
}
|
|
262
|
+
// Find matching ]
|
|
263
|
+
let end = i + 1;
|
|
264
|
+
let inQuote = false;
|
|
265
|
+
let quoteChar = '';
|
|
266
|
+
while (end < part.length) {
|
|
267
|
+
if (inQuote) {
|
|
268
|
+
if (part[end] === quoteChar && part[end - 1] !== '\\') {
|
|
269
|
+
inQuote = false;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
if (part[end] === '"' || part[end] === "'") {
|
|
273
|
+
inQuote = true;
|
|
274
|
+
quoteChar = part[end];
|
|
275
|
+
} else if (part[end] === ']') {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
end++;
|
|
280
|
+
}
|
|
281
|
+
segments.push(part.substring(i + 1, end));
|
|
282
|
+
start = end + 1;
|
|
283
|
+
i = start;
|
|
284
|
+
} else {
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (start < part.length) {
|
|
290
|
+
segments.push(part.substring(start));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return segments.length > 0 ? segments : [part];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private isArrayIndex(segment: string): boolean {
|
|
297
|
+
return /^-?\d+$/.test(segment);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private isArrayAccess(part: string): boolean {
|
|
301
|
+
// Check if the part starts with bracket notation or is a pure number
|
|
302
|
+
return part.startsWith('[') || this.isArrayIndex(part);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private stripQuotes(segment: string): string {
|
|
306
|
+
if ((segment.startsWith('"') && segment.endsWith('"')) ||
|
|
307
|
+
(segment.startsWith("'") && segment.endsWith("'"))) {
|
|
308
|
+
return segment.substring(1, segment.length - 1);
|
|
309
|
+
}
|
|
310
|
+
return segment;
|
|
311
|
+
}
|
|
161
312
|
|
|
162
|
-
private getDataFromArray(el: any, mem: string,
|
|
313
|
+
private getDataFromArray(el: any, mem: string, nextIsArray: boolean): any {
|
|
163
314
|
if (!Array.isArray(el))
|
|
164
315
|
throw new KIRuntimeException(
|
|
165
316
|
StringFormatter.format('Expected an array but found $', el),
|
|
@@ -178,13 +329,13 @@ export class SetFunction extends AbstractFunction {
|
|
|
178
329
|
let je = el[index];
|
|
179
330
|
|
|
180
331
|
if (isNullValue(je)) {
|
|
181
|
-
je =
|
|
332
|
+
je = nextIsArray ? [] : {};
|
|
182
333
|
el[index] = je;
|
|
183
334
|
}
|
|
184
335
|
return je;
|
|
185
336
|
}
|
|
186
337
|
|
|
187
|
-
private getDataFromObject(el: any, mem: string,
|
|
338
|
+
private getDataFromObject(el: any, mem: string, nextIsArray: boolean): any {
|
|
188
339
|
if (Array.isArray(el) || typeof el !== 'object')
|
|
189
340
|
throw new KIRuntimeException(
|
|
190
341
|
StringFormatter.format('Expected an object but found $', el),
|
|
@@ -193,7 +344,7 @@ export class SetFunction extends AbstractFunction {
|
|
|
193
344
|
let je = el[mem];
|
|
194
345
|
|
|
195
346
|
if (isNullValue(je)) {
|
|
196
|
-
je =
|
|
347
|
+
je = nextIsArray ? [] : {};
|
|
197
348
|
el[mem] = je;
|
|
198
349
|
}
|
|
199
350
|
return je;
|