From 9a35302d4bcae112e5fc2684da9ff08609173f43 Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Thu, 22 May 2025 15:37:15 +0530 Subject: [PATCH] Feature: implemented bru.interpolate (#4122) * feat: enhance variable highlighting in CodeMirror and update interpolation method * feat: add interpolate function to bru shim and corresponding tests - Implemented the `interpolate` function in the bru shim to handle variable interpolation. - Added a new test case for the `interpolate` function to verify its functionality with mock variables. * feat: enhance interpolate function to support object interpolation * feat: add translation support for pm.variables.replaceIn to bru.interpolate * revert: eslint config changes * revert: eslint config changes * fix: update method call to use correct interpolation function in Bru class * refactor: added jsdoc to codemirror highlighting code * fix: higlighting for multiline editor --- eslint.config.js | 2 +- package.json | 2 +- .../src/components/CodeEditor/index.js | 5 +- .../src/components/MultiLineEditor/index.js | 2 +- .../RequestPane/GraphQLVariables/index.js | 1 + .../RequestPane/RequestBody/index.js | 7 +-- .../src/components/SingleLineEditor/index.js | 2 +- .../bruno-app/src/utils/common/codemirror.js | 29 +++++++--- .../src/postman/postman-translations.js | 1 + .../src/utils/jscode-shift-translator.js | 2 +- .../postman-comments.spec.js | 4 +- .../transpiler-tests/variables.test.js | 54 +++++++++++++++++++ packages/bruno-js/src/bru.js | 31 +++++------ .../bruno-js/src/runtime/script-runtime.js | 6 +-- .../bruno-js/src/sandbox/quickjs/shims/bru.js | 15 ++++-- .../scripting/api/bru/interpolate.bru | 39 ++++++++++++++ 16 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 packages/bruno-tests/collection/scripting/api/bru/interpolate.bru diff --git a/eslint.config.js b/eslint.config.js index 40f6c3351..0e742fcdf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -38,4 +38,4 @@ module.exports = defineConfig([ "no-undef": "error", }, } -]); +]); \ No newline at end of file diff --git a/package.json b/package.json index b1329d2ee..aba14755d 100644 --- a/package.json +++ b/package.json @@ -71,4 +71,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index f8c13462e..160891542 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -87,7 +87,8 @@ if (!SERVER_RENDERED) { 'bru.runner', 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', - 'bru.runner.stopExecution()' + 'bru.runner.stopExecution()', + 'bru.interpolate(str)' ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); @@ -365,7 +366,7 @@ export default class CodeEditor extends React.Component { let variables = getAllVariables(this.props.collection, this.props.item); this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, mode); + defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index a44caf4ba..1a6709813 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -130,7 +130,7 @@ class MultiLineEditor extends Component { addOverlay = (variables) => { this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, 'text/plain'); + defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js index eaac6f204..d490d8579 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js @@ -67,6 +67,7 @@ const GraphQLVariables = ({ variables, item, collection }) => { mode="javascript" onRun={onRun} onSave={onSave} + enableVariableHighlighting={true} /> ); diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index 8f7fa8465..a0cc8729e 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => { { onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} + enableVariableHighlighting={true} /> ); } if (bodyMode === 'file') { - return + return ; } if (bodyMode === 'formUrlEncoded') { @@ -77,4 +78,4 @@ const RequestBody = ({ item, collection }) => { return No Body; }; -export default RequestBody; \ No newline at end of file +export default RequestBody; diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 16413bdf3..30c079a36 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -146,7 +146,7 @@ class SingleLineEditor extends Component { addOverlay = (variables) => { this.variables = variables; - defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams); + defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true); this.editor.setOption('mode', 'brunovariables'); }; diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index 661b84433..b1a3d5a8a 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -74,11 +74,11 @@ export class MaskedEditor { } else { for (let line = 0; line < lineCount; line++) { const lineLength = this.editor.getLine(line).length; - const maskedNode = document.createTextNode('*'.repeat(lineLength)); + const maskedNode = document.createTextNode('*'.repeat(lineLength)); this.editor.markText( { line, ch: 0 }, { line, ch: lineLength }, - { replacedWith: maskedNode, handleMouseEvents: false } + { replacedWith: maskedNode, handleMouseEvents: false } ); } } @@ -86,7 +86,18 @@ export class MaskedEditor { }; } -export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => { +/** + * Defines a custom CodeMirror mode for Bruno variables highlighting. + * This function creates a specialized mode that can highlight both Bruno template + * variables (in the format {{variable}}) and URL path parameters (in the format /:param). + * + * @param {Object} _variables - The variables object containing data to validate against + * @param {string} mode - The base CodeMirror mode to extend (e.g., 'javascript', 'application/json') + * @param {boolean} highlightPathParams - Whether to highlight URL path parameters + * @param {boolean} highlightVariables - Whether to highlight template variables + * @returns {void} - Registers the mode with CodeMirror for later use + */ +export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams, highlightVariables) => { CodeMirror.defineMode('brunovariables', function (config, parserConfig) { const { pathParams = {}, ...variables } = _variables || {}; const variablesOverlay = { @@ -139,13 +150,15 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa } }; - let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay); + let baseMode = CodeMirror.getMode(config, parserConfig.backdrop || mode); - if (highlightPathParams) { - return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay); - } else { - return baseMode; + if (highlightVariables) { + baseMode = CodeMirror.overlayMode(baseMode, variablesOverlay); } + if (highlightPathParams) { + baseMode = CodeMirror.overlayMode(baseMode, urlPathParamsOverlay); + } + return baseMode; }); }; diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index a7bb02fcd..252c4c2d3 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -5,6 +5,7 @@ const replacements = { 'pm\\.environment\\.set\\(': 'bru.setEnvVar(', 'pm\\.variables\\.get\\(': 'bru.getVar(', 'pm\\.variables\\.set\\(': 'bru.setVar(', + 'pm\\.variables\\.replaceIn\\(': 'bru.interpolate(', 'pm\\.collectionVariables\\.get\\(': 'bru.getVar(', 'pm\\.collectionVariables\\.set\\(': 'bru.setVar(', 'pm\\.collectionVariables\\.has\\(': 'bru.hasVar(', diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 6a892e516..92ccf97ba 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -52,7 +52,7 @@ const simpleTranslations = { 'pm.variables.get': 'bru.getVar', 'pm.variables.set': 'bru.setVar', 'pm.variables.has': 'bru.hasVar', - + 'pm.variables.replaceIn': 'bru.interpolate', // Collection variables 'pm.collectionVariables.get': 'bru.getVar', 'pm.collectionVariables.set': 'bru.setVar', diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js index 1c1686bf2..fed9f2931 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js @@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => { }); test('should comment non-translated pm commands', () => { - const inputScript = "pm.test('random test', () => postman.variables.replaceIn('{{$guid}}'));"; - const expectedOutput = "// test('random test', () => pm.variables.replaceIn('{{$guid}}'));"; + const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));"; + const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js index b4439f826..fe0f80593 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js @@ -5,55 +5,104 @@ describe('Variables Translation', () => { it('should translate pm.variables.get', () => { const code = 'pm.variables.get("test");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("test");'); }); it('should translate pm.variables.set', () => { const code = 'pm.variables.set("test", "value");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("test", "value");'); }); it('should translate pm.variables.has', () => { const code = 'pm.variables.has("userId");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("userId");'); }); + it('should translate pm.variables.replaceIn', () => { + const code = 'pm.variables.replaceIn("Hello {{name}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('bru.interpolate("Hello {{name}}");'); + }); + + it('should translate pm.variables.replaceIn with variables and expressions', () => { + const code = 'const greeting = pm.variables.replaceIn("Hello {{name}}, your user id is {{userId}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const greeting = bru.interpolate("Hello {{name}}, your user id is {{userId}}");'); + }); + + it('should translate pm.variables.replaceIn within complex expressions', () => { + const code = 'const url = baseUrl + pm.variables.replaceIn("/users/{{userId}}/profile");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const url = baseUrl + bru.interpolate("/users/{{userId}}/profile");'); + }); + + it('should translate pm.variables.replaceIn with multiple nested variable references', () => { + const code = 'const template = pm.variables.replaceIn("{{prefix}}-{{env}}-{{suffix}}");'; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe('const template = bru.interpolate("{{prefix}}-{{env}}-{{suffix}}");'); + }); + + it('should translate aliased variables.replaceIn', () => { + const code = ` + const variables = pm.variables; + const message = variables.replaceIn("Welcome, {{username}}!"); + `; + const translatedCode = translateCode(code); + + expect(translatedCode).toBe(` + const message = bru.interpolate("Welcome, {{username}}!"); + `); + }); + // Collection variables tests it('should translate pm.collectionVariables.get', () => { const code = 'pm.collectionVariables.get("apiUrl");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getVar("apiUrl");'); }); it('should translate pm.collectionVariables.set', () => { const code = 'pm.collectionVariables.set("token", jsonData.token);'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("token", jsonData.token);'); }); it('should translate pm.collectionVariables.has', () => { const code = 'pm.collectionVariables.has("authToken");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.hasVar("authToken");'); }); it('should translate pm.collectionVariables.unset', () => { const code = 'pm.collectionVariables.unset("tempVar");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.deleteVar("tempVar");'); }); it('should handle pm.globals.get', () => { const code = 'pm.globals.get("test");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.getGlobalEnvVar("test");'); }); it('should handle pm.globals.set', () => { const code = 'pm.globals.set("test", "value");'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setGlobalEnvVar("test", "value");'); }); @@ -66,6 +115,7 @@ describe('Variables Translation', () => { const get = vars.get("test"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const has = bru.hasVar("test"); const set = bru.setVar("test", "value"); @@ -83,6 +133,7 @@ describe('Variables Translation', () => { const unset = collVars.unset("test"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const has = bru.hasVar("test"); const set = bru.setVar("test", "value"); @@ -98,6 +149,7 @@ describe('Variables Translation', () => { const set = globals.set("test", "value"); `; const translatedCode = translateCode(code); + expect(translatedCode).toBe(` const get = bru.getGlobalEnvVar("test"); const set = bru.setGlobalEnvVar("test", "value"); @@ -108,6 +160,7 @@ describe('Variables Translation', () => { it('should handle conditional expressions with variable calls', () => { const code = 'const userStatus = pm.variables.has("userId") ? "logged-in" : "guest";'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('const userStatus = bru.hasVar("userId") ? "logged-in" : "guest";'); }); @@ -148,6 +201,7 @@ describe('Variables Translation', () => { it('should handle more complex nested expressions with variables', () => { const code = 'pm.collectionVariables.set("fullPath", pm.environment.get("baseUrl") + pm.variables.get("endpoint"));'; const translatedCode = translateCode(code); + expect(translatedCode).toBe('bru.setVar("fullPath", bru.getEnvVar("baseUrl") + bru.getVar("endpoint"));'); }); }); \ No newline at end of file diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 77255b3a1..d38d28983 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,5 +1,5 @@ const { cloneDeep } = require('lodash'); -const { interpolate } = require('@usebruno/common'); +const { interpolate: _interpolate } = require('@usebruno/common'); const variableNameRegex = /^[\w-.]*$/; @@ -28,10 +28,10 @@ class Bru { }; } - _interpolate = (str) => { - if (!str || !str.length || typeof str !== 'string') { - return str; - } + interpolate = (strOrObj) => { + if (!strOrObj) return strOrObj; + const isObj = typeof strOrObj === 'object'; + const strToInterpolate = isObj ? JSON.stringify(strOrObj) : strOrObj; const combinedVars = { ...this.globalEnvironmentVariables, @@ -48,7 +48,8 @@ class Bru { } }; - return interpolate(str, combinedVars); + const interpolatedStr = _interpolate(strToInterpolate, combinedVars); + return isObj ? JSON.parse(interpolatedStr) : interpolatedStr; }; cwd() { @@ -68,7 +69,7 @@ class Bru { } getEnvVar(key) { - return this._interpolate(this.envVariables[key]); + return this.interpolate(this.envVariables[key]); } setEnvVar(key, value) { @@ -84,7 +85,7 @@ class Bru { } getGlobalEnvVar(key) { - return this._interpolate(this.globalEnvironmentVariables[key]); + return this.interpolate(this.globalEnvironmentVariables[key]); } setGlobalEnvVar(key, value) { @@ -96,7 +97,7 @@ class Bru { } getOauth2CredentialVar(key) { - return this._interpolate(this.oauth2CredentialVariables[key]); + return this.interpolate(this.oauth2CredentialVariables[key]); } hasVar(key) { @@ -111,7 +112,7 @@ class Bru { if (variableNameRegex.test(key) === false) { throw new Error( `Variable name: "${key}" contains invalid characters!` + - ' Names must only contain alpha-numeric characters, "-", "_", "."' + ' Names must only contain alpha-numeric characters, "-", "_", "."' ); } @@ -122,11 +123,11 @@ class Bru { if (variableNameRegex.test(key) === false) { throw new Error( `Variable name: "${key}" contains invalid characters!` + - ' Names must only contain alpha-numeric characters, "-", "_", "."' + ' Names must only contain alpha-numeric characters, "-", "_", "."' ); } - return this._interpolate(this.runtimeVariables[key]); + return this.interpolate(this.runtimeVariables[key]); } deleteVar(key) { @@ -142,15 +143,15 @@ class Bru { } getCollectionVar(key) { - return this._interpolate(this.collectionVariables[key]); + return this.interpolate(this.collectionVariables[key]); } getFolderVar(key) { - return this._interpolate(this.folderVariables[key]); + return this.interpolate(this.folderVariables[key]); } getRequestVar(key) { - return this._interpolate(this.requestVariables[key]); + return this.interpolate(this.requestVariables[key]); } setNextRequest(nextRequest) { diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 1d9680bab..a8f2abbee 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -98,7 +98,7 @@ class ScriptRuntime { }; } - if(runRequestByItemPathname) { + if (runRequestByItemPathname) { context.bru.runRequest = runRequestByItemPathname; } @@ -151,7 +151,7 @@ class ScriptRuntime { chai, 'node-fetch': fetch, 'crypto-js': CryptoJS, - 'xml2js': xml2js, + xml2js: xml2js, cheerio, tv4, ...whitelistedModules, @@ -235,7 +235,7 @@ class ScriptRuntime { }; } - if(runRequestByItemPathname) { + if (runRequestByItemPathname) { context.bru.runRequest = runRequestByItemPathname; } diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index b8ffa76ab..8439d7206 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -29,6 +29,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getProcessEnv', getProcessEnv); getProcessEnv.dispose(); + let interpolate = vm.newFunction('interpolate', function (str) { + return marshallToVm(bru.interpolate(vm.dump(str)), vm); + }); + vm.setProp(bruObject, 'interpolate', interpolate); + interpolate.dispose(); + let hasEnvVar = vm.newFunction('hasEnvVar', function (key) { return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm); }); @@ -157,7 +163,8 @@ const addBruShimToContext = (vm, bru) => { let getTestResults = vm.newFunction('getTestResults', () => { const promise = vm.newPromise(); - bru.getTestResults() + bru + .getTestResults() .then((results) => { promise.resolve(marshallToVm(cleanJson(results), vm)); }) @@ -178,7 +185,8 @@ const addBruShimToContext = (vm, bru) => { let getAssertionResults = vm.newFunction('getAssertionResults', () => { const promise = vm.newPromise(); - bru.getAssertionResults() + bru + .getAssertionResults() .then((results) => { promise.resolve(marshallToVm(cleanJson(results), vm)); }) @@ -199,7 +207,8 @@ const addBruShimToContext = (vm, bru) => { let runRequestHandle = vm.newFunction('runRequest', (args) => { const promise = vm.newPromise(); - bru.runRequest(vm.dump(args)) + bru + .runRequest(vm.dump(args)) .then((response) => { const { status, headers, data, dataBuffer, size, statusText } = response || {}; promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm)); diff --git a/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru b/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru new file mode 100644 index 000000000..a8e6dff76 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/interpolate.bru @@ -0,0 +1,39 @@ +meta { + name: interpolate + type: http + seq: 13 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +tests { + test("should interpolate envs", function() { + const interpolated = bru.interpolate("url: {{host}}") + expect(interpolated).to.equal("url: https://testbench-sanity.usebruno.com"); + }); + + test("should interpolate random variables", function() { + const a = bru.interpolate("{{$randomInt}}") + const b = bru.interpolate("{{$randomInt}}") + expect(a).to.not.equal(b) + }); + + const randomObj = { + host: "{{host}}", + int: "{{$randomInt}}", + timestamp: "{{$timestamp}}" + } + + test("should interpolate objects with vars, random vars", function() { + const objA = bru.interpolate(randomObj) + const objB = bru.interpolate(randomObj) + + expect(objA).to.be.an("object") + expect(objB).to.be.an("object") + expect(objA).to.not.deep.eql(objB) + }); +}