UNPKG

7.03 kBPlain TextView Raw
1import { runInNewContext } from 'vm';
2import { getLogger, LoggingContext } from './logging';
3import { nodeGlobals } from './nodeGlobals';
4
5export 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
26function 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
53export 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 // if the string contains only number description, that can be converted, then return a number, in other case return a string
74 if (
75 foundNumericExpression &&
76 afterEvalToNumber === (+afterEvalToNumber).toString()
77 ) {
78 return +afterEvalToNumber;
79 }
80 return afterEvalToNumber;
81}
82
83export 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 // contains nested values
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
108function 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
119function injectEvaluationToString(
120 str: string,
121 ctx: LoggingContext,
122 vars: Map<string, unknown>,
123): string {
124 // the vars (scenario variable) should be left available here in order to access
125 // and set them from within evaluated expressions
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
151function 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
185function 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