From 271f988d99ad3c99ac6842319c874428422eab58 Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Lima <36305985+leoferreiralima@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:38:14 -0300 Subject: [PATCH] feat: add recursive interpolate (#2234) * feat: add recursive interpolate fixes #2227 * test(bruno-common): fix test with 3 level of recursion * fix(bruno-common): add ability to reference the same variable repeatedly --- .../src/utils/codemirror/brunoVarInfo.js | 7 +- packages/bruno-common/package.json | 1 + .../src/interpolate/index.spec.ts | 167 ++++++++++++++++++ .../bruno-common/src/interpolate/index.ts | 37 +++- 4 files changed, 206 insertions(+), 6 deletions(-) diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index d37e10bb66..045c6acf7f 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -6,6 +6,11 @@ * LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3 */ +// Todo: Fix this +// import { interpolate } from '@usebruno/common'; +import brunoCommon from '@usebruno/common'; +const { interpolate } = brunoCommon; + let CodeMirror; const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const { get } = require('lodash'); @@ -21,7 +26,7 @@ if (!SERVER_RENDERED) { // str is of format {{variableName}}, extract variableName // we are seeing that from the gql query editor, the token string is of format variableName const variableName = str.replace('{{', '').replace('}}', '').trim(); - const variableValue = get(options.variables, variableName); + const variableValue = interpolate(get(options.variables, variableName), options.variables); const into = document.createElement('div'); const descriptionDiv = document.createElement('div'); diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json index d8e598420e..cc25f2337c 100644 --- a/packages/bruno-common/package.json +++ b/packages/bruno-common/package.json @@ -13,6 +13,7 @@ "scripts": { "clean": "rimraf dist", "test": "jest", + "test:watch": "jest --watch", "prebuild": "npm run clean", "build": "rollup -c", "prepack": "npm run test && npm run build" diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts index 9779021ee8..a1994d4731 100644 --- a/packages/bruno-common/src/interpolate/index.spec.ts +++ b/packages/bruno-common/src/interpolate/index.spec.ts @@ -169,3 +169,170 @@ describe('interpolate - value edge cases', () => { expect(result).toBe(inputString); }); }); + +describe('interpolate - recursive', () => { + it('should replace placeholders with 1 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + 'user.name': 'Bruno', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno and I am 4 years old'); + }); + + it('should replace placeholders with 2 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + 'user.name': 'Bruno {{user.lastName}}', + 'user.lastName': 'Dog', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old'); + }); + + it('should replace placeholders with 3 level of recursion with values from the object', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.full_name}} and I am {{user.age}} years old', + 'user.full_name': '{{user.name}}', + 'user.name': 'Bruno {{user.lastName}}', + 'user.lastName': 'Dog', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is Bruno Dog and I am 4 years old'); + }); + + it('should handle missing values with 1 level of recursion by leaving the placeholders unchanged using {{}} as delimiters', () => { + const inputString = '{{user.message}}'; + const inputObject = { + 'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old', + user: { + age: 4 + } + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('Hello, my name is {{user.name}} and I am 4 years old'); + }); + + it('should handle all valid keys with 1 level of recursion', () => { + const message = ` + Hi, I am {{user.full_name}}, + I am {{user.age}} years old. + My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}. + I like attention: {{user.want.attention}} +`; + const inputObject = { + user: { + message, + full_name: 'Bruno', + age: 4, + 'fav-food': ['egg', 'meat'], + 'want.attention': true + } + }; + + const inputStr = '{{user.message}}'; + const expectedStr = ` + Hi, I am Bruno, + I am 4 years old. + My favorite food is egg and meat. + I like attention: true +`; + const result = interpolate(inputStr, inputObject); + expect(result).toBe(expectedStr); + }); + + it('should not process 1 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion}}'); + }); + + it('should not process 2 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion2}}', + recursion2: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion2}}'); + }); + + it('should not process 3 level of cycle recursion with values from the object', () => { + const inputString = '{{recursion}}'; + const inputObject = { + recursion: '{{recursion2}}', + recursion2: '{{recursion3}}', + recursion3: '{{recursion}}' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe('{{recursion2}}'); + }); + + it('should replace repetead placeholders with 1 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: 'repetead2' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(2).fill('repetead2').join(' ')); + }); + + it('should replace repetead placeholders with 2 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}', + repetead3: 'repetead3' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(6).fill('repetead3').join(' ')); + }); + + it('should replace repetead placeholders with 3 level of recursion with values from the object', () => { + const inputString = '{{repetead}}'; + const inputObject = { + repetead: '{{repetead2}} {{repetead2}}', + repetead2: '{{repetead3}} {{repetead3}} {{repetead3}}', + repetead3: '{{repetead4}} {{repetead4}} {{repetead4}} {{repetead4}}', + repetead4: 'repetead4' + }; + + const result = interpolate(inputString, inputObject); + + expect(result).toBe(new Array(24).fill('repetead4').join(' ')); + }); +}); diff --git a/packages/bruno-common/src/interpolate/index.ts b/packages/bruno-common/src/interpolate/index.ts index 8ad86c5b1b..d4bd7cb6b0 100644 --- a/packages/bruno-common/src/interpolate/index.ts +++ b/packages/bruno-common/src/interpolate/index.ts @@ -11,6 +11,7 @@ * Output: Hello, my name is Bruno and I am 4 years old */ +import { Set } from 'typescript'; import { flattenObject } from '../utils'; const interpolate = (str: string, obj: Record): string => { @@ -18,14 +19,40 @@ const interpolate = (str: string, obj: Record): string => { return str; } - const patternRegex = /\{\{([^}]+)\}\}/g; const flattenedObj = flattenObject(obj); - const result = str.replace(patternRegex, (match, placeholder) => { + + return replace(str, flattenedObj); +}; + +const replace = ( + str: string, + flattenedObj: Record, + visited = new Set(), + results = new Map() +): string => { + const patternRegex = /\{\{([^}]+)\}\}/g; + + return str.replace(patternRegex, (match, placeholder) => { const replacement = flattenedObj[placeholder]; - return replacement !== undefined ? replacement : match; - }); - return result; + if (results.has(match)) { + return results.get(match); + } + + if (patternRegex.test(replacement) && !visited.has(match)) { + visited.add(match); + const result = replace(replacement, flattenedObj, visited, results); + results.set(match, result); + + return result; + } + + visited.add(match); + const result = replacement !== undefined ? replacement : match; + results.set(match, result); + + return result; + }); }; export default interpolate;