1 | import { runInNewContext } from 'vm';
|
2 | import { getLogger, LoggingContext } from './logging';
|
3 | import { nodeGlobals } from './nodeGlobals';
|
4 |
|
5 | export function injectVariableAccessAndEvaluate(
|
6 | expression: string,
|
7 | scenarioVariables: Map<string, unknown>,
|
8 | ): unknown {
|
9 | const regex = /{{(\w*)}}/g;
|
10 | const variables: { [key: string]: unknown } = {};
|
11 | const expressionWithVariables = searchForMatchingStrings(
|
12 | regex,
|
13 | expression,
|
14 | ).reduce((previousString, variable) => {
|
15 | variables[variable] = scenarioVariables.get(variable);
|
16 | const searchValue = `{{${variable}}}`;
|
17 | return previousString.replace(searchValue, variable);
|
18 | }, expression);
|
19 |
|
20 | return runInNewContext(expressionWithVariables, {
|
21 | ...nodeGlobals,
|
22 | ...variables,
|
23 | });
|
24 | }
|
25 |
|
26 | function injectVarsToString(
|
27 | str: string,
|
28 | scenarioVariables: Map<string, unknown>,
|
29 | ctx: LoggingContext,
|
30 | ): string {
|
31 | const regex = /{{(\w*)}}/g;
|
32 | return searchForMatchingStrings(regex, str).reduce(
|
33 | (previousString, variable) => {
|
34 | const replaceValue = scenarioVariables.get(variable);
|
35 | if (replaceValue !== undefined) {
|
36 | const searchValue = `{{${variable}}}`;
|
37 | getLogger(ctx.scenario).debug(
|
38 | `Replacing '${searchValue}' with '${replaceValue}'`,
|
39 | ctx,
|
40 | );
|
41 | return previousString.replace(searchValue, `${replaceValue}`);
|
42 | }
|
43 | getLogger(ctx.scenario).debug(
|
44 | `Not able to replace {{${variable}}} because no variable with that name found!`,
|
45 | ctx,
|
46 | );
|
47 | return previousString;
|
48 | },
|
49 | str,
|
50 | );
|
51 | }
|
52 |
|
53 | export function injectEvalAndVarsToString(
|
54 | str: string,
|
55 | scenarioVariables: Map<string, unknown>,
|
56 | ctx: LoggingContext,
|
57 | ): string | number {
|
58 | const afterEvalToString = injectEvaluationToString(
|
59 | str,
|
60 | ctx,
|
61 | scenarioVariables,
|
62 | );
|
63 | const afterVarInjection = injectVarsToString(
|
64 | afterEvalToString,
|
65 | scenarioVariables,
|
66 | ctx,
|
67 | );
|
68 | const [
|
69 | afterEvalToNumber,
|
70 | foundNumericExpression,
|
71 | ] = injectEvaluationToNumber(afterVarInjection, ctx, scenarioVariables);
|
72 |
|
73 |
|
74 | if (
|
75 | foundNumericExpression &&
|
76 | afterEvalToNumber === (+afterEvalToNumber).toString()
|
77 | ) {
|
78 | return +afterEvalToNumber;
|
79 | }
|
80 | return afterEvalToNumber;
|
81 | }
|
82 |
|
83 | export function injectEvalAndVarsToMap(
|
84 | keyValueMap: any,
|
85 | scenarioVariables: Map<string, unknown>,
|
86 | ctx: LoggingContext,
|
87 | ): any {
|
88 | const copy = keyValueMap instanceof Array ? [] : {};
|
89 | Object.assign(copy, keyValueMap);
|
90 | for (const mapEntry of Object.entries(copy)) {
|
91 | const key = mapEntry[0];
|
92 | const value = mapEntry[1];
|
93 |
|
94 | if (value instanceof Object) {
|
95 |
|
96 | copy[key] = injectEvalAndVarsToMap(value, scenarioVariables, ctx);
|
97 | } else if (typeof value === 'string') {
|
98 | copy[key] = injectEvalAndVarsToString(
|
99 | value,
|
100 | scenarioVariables,
|
101 | ctx,
|
102 | );
|
103 | }
|
104 | }
|
105 | return copy;
|
106 | }
|
107 |
|
108 | function searchForMatchingStrings(regex: RegExp, str: string): string[] {
|
109 | const regexCopy = regex;
|
110 | let m;
|
111 | const matchingStrings: string[] = [];
|
112 | while ((m = regexCopy.exec(str)) !== null) {
|
113 | if (m.index === regexCopy.lastIndex) regexCopy.lastIndex += 1;
|
114 | matchingStrings.push(m[1]);
|
115 | }
|
116 | return matchingStrings;
|
117 | }
|
118 |
|
119 | function injectEvaluationToString(
|
120 | str: string,
|
121 | ctx: LoggingContext,
|
122 | vars: Map<string, unknown>,
|
123 | ): string {
|
124 |
|
125 |
|
126 |
|
127 | const regex = /{{{(.*?)}}}/g;
|
128 | let result = str;
|
129 | searchForMatchingStrings(regex, result).forEach(expression => {
|
130 | const replaceValue = (() => eval(expression)).call(
|
131 | buildExpHelpers(vars),
|
132 | );
|
133 | if (replaceValue) {
|
134 | const searchValue = `{{{${expression}}}}`;
|
135 | getLogger(ctx.scenario).debug(
|
136 | `Replacing '${searchValue}' with '${replaceValue}'`,
|
137 | ctx,
|
138 | );
|
139 | result = result.replace(searchValue, replaceValue);
|
140 | } else {
|
141 | getLogger(ctx.scenario).debug(
|
142 | `Not able to replace {{{${expression}}}} !`,
|
143 | ctx,
|
144 | );
|
145 | }
|
146 | });
|
147 |
|
148 | return result;
|
149 | }
|
150 |
|
151 | function injectEvaluationToNumber(
|
152 | str: string,
|
153 | ctx: LoggingContext,
|
154 | vars: Map<string, unknown>,
|
155 | ): [string, boolean] {
|
156 | // the vars (scenario variable) should be left available here in order to access
|
157 | // and set them from within evaluated expressions
|
158 |
|
159 | const regex = /<<<(.*?)>>>/g;
|
160 | let foundNumericExpression = false;
|
161 | let result = str;
|
162 | searchForMatchingStrings(regex, result).forEach(expression => {
|
163 | foundNumericExpression = true;
|
164 | const replaceValue = (() => eval(expression)).call(
|
165 | buildExpHelpers(vars),
|
166 | );
|
167 | if (replaceValue) {
|
168 | const searchValue = `<<<${expression}>>>`;
|
169 | getLogger(ctx.scenario).debug(
|
170 | `Replacing '"${searchValue}"' with '${replaceValue}'`,
|
171 | ctx,
|
172 | );
|
173 | result = result.replace(searchValue, replaceValue);
|
174 | } else {
|
175 | getLogger(ctx.scenario).debug(
|
176 | `Not able to replace {{{${expression}}}} !`,
|
177 | ctx,
|
178 | );
|
179 | }
|
180 | });
|
181 |
|
182 | return [result, foundNumericExpression];
|
183 | }
|
184 |
|
185 | function buildExpHelpers(vars: Map<string, any>): unknown {
|
186 | return {
|
187 | get(name: string): any {
|
188 | return vars.get(name);
|
189 | },
|
190 | set(name: string, value: any) {
|
191 | return vars.set(name, value);
|
192 | },
|
193 | getAndInc(name: string): number {
|
194 | const currentValue = vars.get(name);
|
195 | vars.set(name, currentValue + 1);
|
196 | return currentValue;
|
197 | },
|
198 | getAndIncBy(name: string, howMuch: number): number {
|
199 | const currentValue = vars.get(name);
|
200 | vars.set(name, currentValue + howMuch);
|
201 | return currentValue;
|
202 | },
|
203 | incAndGet(name: string): number {
|
204 | const targetValue = vars.get(name) + 1;
|
205 | vars.set(name, targetValue);
|
206 | return targetValue;
|
207 | },
|
208 | incByAndGet(name: string, howMuch: number): number {
|
209 | const targetValue = vars.get(name) + howMuch;
|
210 | vars.set(name, targetValue);
|
211 | return targetValue;
|
212 | },
|
213 | datePlusMinutesIso(minutes: number): string {
|
214 | return new Date(Date.now() + minutes * 60e3).toISOString();
|
215 | },
|
216 | timestampPlusMinutes(minutes: number): number {
|
217 | return Date.now() + minutes * 60e3;
|
218 | },
|
219 | };
|
220 | }
|
221 |
|
\ | No newline at end of file |