Skip to content

Commit 5c8a6fb

Browse files
authored
fix(util-endpoints): evaluateTemplate implementation without RegExp/Function (#4136)
1 parent 8e10769 commit 5c8a6fb

File tree

3 files changed

+57
-36
lines changed

3 files changed

+57
-36
lines changed

packages/util-endpoints/src/utils/evaluateErrorRule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ export const evaluateErrorRule = (errorRule: ErrorRuleObject, options: EvaluateO
1414
evaluateExpression(error, "Error", {
1515
...options,
1616
referenceRecord: { ...options.referenceRecord, ...referenceRecord },
17-
})
17+
}) as string
1818
);
1919
};

packages/util-endpoints/src/utils/evaluateTemplate.spec.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ describe(evaluateTemplate.name, () => {
1313
jest.clearAllMocks();
1414
});
1515

16-
it("should escape tilde while processing expression", () => {
17-
const template = "foo `bar` baz";
18-
// This test verifies that tilde is unescaped after processing.
19-
expect(evaluateTemplate(template, mockOptions)).toBe(template);
20-
expect(getAttr).not.toHaveBeenCalled();
16+
it("should not escape template without braces", () => {
17+
const templateWithoutBraces = "foo bar baz";
18+
expect(evaluateTemplate(templateWithoutBraces, mockOptions)).toEqual(templateWithoutBraces);
2119
});
2220

2321
describe("should replace `{parameterName}` with value", () => {
@@ -33,7 +31,7 @@ describe(evaluateTemplate.name, () => {
3331
});
3432
});
3533

36-
it("should not replace string escaped {{value}}", () => {
34+
it("should escape values within double braces like {{value}}", () => {
3735
const value = "bar";
3836
expect(evaluateTemplate("foo {{value1}} bar {{value2}} baz", { ...mockOptions, endpointParams: { value } })).toBe(
3937
"foo {value1} bar {value2} baz"
@@ -56,4 +54,15 @@ describe(evaluateTemplate.name, () => {
5654
expect(getAttr).toHaveBeenNthCalledWith(1, ref1, "key1");
5755
expect(getAttr).toHaveBeenNthCalledWith(2, ref2, "key2");
5856
});
57+
58+
describe("should not change template with incomplete braces", () => {
59+
it.each([
60+
"incomplete opening bracket '{' in template",
61+
"incomplete closing bracket '}' in template",
62+
"incomplete opening escape '{{' in template",
63+
"incomplete closing escape '}}' in template",
64+
])("%s", (template) => {
65+
expect(evaluateTemplate(template, mockOptions)).toEqual(template);
66+
});
67+
});
5968
});
Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,50 @@
11
import { getAttr } from "../lib";
22
import { EvaluateOptions } from "../types";
33

4-
const ATTR_SHORTHAND_REGEX = new RegExp("\\${([\\w]+)#([\\w]+)}", "g");
5-
64
export const evaluateTemplate = (template: string, options: EvaluateOptions) => {
7-
const templateToEvaluate = template
8-
// Replace `{value}` with `${value}`
9-
.replace(new RegExp(`\{([^{}]+)\}`, "g"), "${$1}")
10-
// Replace `{${value}}` with `{value}`
11-
.replace(new RegExp(`\{\\$\{([^{}]+)\}\}`, "g"), "{$1}");
5+
const evaluatedTemplateArr: string[] = [];
126

137
const templateContext = {
148
...options.endpointParams,
159
...options.referenceRecord,
16-
};
17-
18-
const attrShortHandList = templateToEvaluate.match(ATTR_SHORTHAND_REGEX) || [];
19-
20-
const attrShortHandMap = attrShortHandList.reduce((acc, attrShortHand) => {
21-
const indexOfHash = attrShortHand.indexOf("#");
22-
const refName = attrShortHand.substring(2, indexOfHash);
23-
const attrName = attrShortHand.substring(indexOfHash + 1, attrShortHand.length - 1);
24-
acc[attrShortHand] = getAttr(templateContext[refName] as Record<string, any>, attrName) as string;
25-
return acc;
26-
}, {} as Record<string, string>);
27-
28-
const templateWithAttr = Object.entries(attrShortHandMap).reduce(
29-
(acc, [shortHand, value]) => acc.replace(shortHand, value),
30-
templateToEvaluate
31-
);
32-
33-
const templateContextNames = Object.keys(templateContext);
34-
const templateContextValues = Object.values(templateContext);
35-
const templateWithTildeEscaped = templateWithAttr.replace(/\`/g, "\\`");
36-
37-
return new Function(...templateContextNames, `return \`${templateWithTildeEscaped}\``)(...templateContextValues);
10+
} as Record<string, string>;
11+
12+
let currentIndex = 0;
13+
while (currentIndex < template.length) {
14+
const openingBraceIndex = template.indexOf("{", currentIndex);
15+
16+
if (openingBraceIndex === -1) {
17+
// No more opening braces, add the rest of the template and break.
18+
evaluatedTemplateArr.push(template.slice(currentIndex));
19+
break;
20+
}
21+
22+
evaluatedTemplateArr.push(template.slice(currentIndex, openingBraceIndex));
23+
const closingBraceIndex = template.indexOf("}", openingBraceIndex);
24+
25+
if (closingBraceIndex === -1) {
26+
// No more closing braces, add the rest of the template and break.
27+
evaluatedTemplateArr.push(template.slice(openingBraceIndex));
28+
break;
29+
}
30+
31+
if (template[openingBraceIndex + 1] === "{" && template[closingBraceIndex + 1] === "}") {
32+
// Escaped expression. Do not evaluate.
33+
evaluatedTemplateArr.push(template.slice(openingBraceIndex + 1, closingBraceIndex));
34+
currentIndex = closingBraceIndex + 2;
35+
}
36+
37+
const parameterName = template.substring(openingBraceIndex + 1, closingBraceIndex);
38+
39+
if (parameterName.includes("#")) {
40+
const [refName, attrName] = parameterName.split("#");
41+
evaluatedTemplateArr.push(getAttr(templateContext[refName], attrName) as string);
42+
} else {
43+
evaluatedTemplateArr.push(templateContext[parameterName]);
44+
}
45+
46+
currentIndex = closingBraceIndex + 1;
47+
}
48+
49+
return evaluatedTemplateArr.join("");
3850
};

0 commit comments

Comments
 (0)