From b1e6a707bfad5d9d6538796dd797c4a6d6e23fac Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:58:03 +0530 Subject: [PATCH] feat: add support for interpolation on `mockDataFunctions` (#6393) feat: implement `prepareMockObj` function for enhanced mock data processing in interpolation --- .../src/interpolate/index.spec.ts | 87 +++++++++++++++++++ .../bruno-common/src/interpolate/index.ts | 81 ++++++++++++----- 2 files changed, 144 insertions(+), 24 deletions(-) diff --git a/packages/bruno-common/src/interpolate/index.spec.ts b/packages/bruno-common/src/interpolate/index.spec.ts index 98d4e3223..dac68d3ff 100644 --- a/packages/bruno-common/src/interpolate/index.spec.ts +++ b/packages/bruno-common/src/interpolate/index.spec.ts @@ -375,6 +375,62 @@ describe('interpolate - recursive', () => { "x": "baz bar" }`); }); + + it('should replace variables pointing to mock data functions', () => { + const inputString = 'Timestamp: {{folderVar}}'; + const inputObject = { + folderVar: '{{$isoTimestamp}}' + }; + + const result = interpolate(inputString, inputObject); + + // Validate that the result is a valid ISO timestamp + const timestampPattern = /^Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + expect(timestampPattern.test(result)).toBe(true); + }); + + it('should replace nested variables pointing to mock data functions', () => { + const inputString = 'Random values: {{var1}} and {{var2}}'; + const inputObject = { + var1: '{{nestedVar}}', + nestedVar: '{{$randomInt}}', + var2: '{{$randomBoolean}}' + }; + + const result = interpolate(inputString, inputObject); + + // Validate the result + const parts = result.split(' and '); + expect(parts.length).toBe(2); + + const randomInt = parts[0].replace('Random values: ', ''); + const randomBoolean = parts[1]; + + // Check if randomInt is a number + expect(!isNaN(Number(randomInt))).toBe(true); + expect(Number(randomInt)).toBeGreaterThanOrEqual(0); + expect(Number(randomInt)).toBeLessThanOrEqual(1000); + + // Check if randomBoolean is a boolean + expect(['true', 'false'].includes(randomBoolean)).toBe(true); + }); + + it('should replace variables pointing to mock data functions with escapeJSONStrings option', () => { + const inputString = '{"timestamp": "{{folderVar}}"}'; + const inputObject = { + folderVar: '{{$isoTimestamp}}' + }; + + const result = interpolate(inputString, inputObject, { escapeJSONStrings: true }); + + // Should produce valid JSON + expect(() => { + const parsed = JSON.parse(result); + // Validate that the timestamp is a valid ISO timestamp + const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + expect(timestampPattern.test(parsed.timestamp)).toBe(true); + }).not.toThrow(); + }); }); describe('interpolate - object handling', () => { @@ -534,6 +590,37 @@ describe('interpolate - mock variable interpolation', () => { JSON.parse(result); // This should throw an error }).toThrow(); }); + + it('should process mock variables in nested objects', () => { + const inputString = '{{user.data}}'; + const inputObject = { + user: { + data: { + id: '{{$randomUUID}}', + timestamp: '{{$isoTimestamp}}', + nested: { + randomInt: '{{$randomInt}}' + } + } + } + }; + + const result = interpolate(inputString, inputObject); + const parsed = JSON.parse(result); + + // Validate UUID format + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + expect(uuidPattern.test(parsed.id)).toBe(true); + + // Validate ISO timestamp format + const isoTimestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + expect(isoTimestampPattern.test(parsed.timestamp)).toBe(true); + + // Validate nested randomInt + expect(!isNaN(Number(parsed.nested.randomInt))).toBe(true); + expect(Number(parsed.nested.randomInt)).toBeGreaterThanOrEqual(0); + expect(Number(parsed.nested.randomInt)).toBeLessThanOrEqual(1000); + }); }); describe('interpolate - Date() handling', () => { diff --git a/packages/bruno-common/src/interpolate/index.ts b/packages/bruno-common/src/interpolate/index.ts index 4cd8a2b16..288f06c3b 100644 --- a/packages/bruno-common/src/interpolate/index.ts +++ b/packages/bruno-common/src/interpolate/index.ts @@ -12,7 +12,58 @@ */ import { mockDataFunctions } from '../utils/faker-functions'; -import { get } from 'lodash-es'; +import { get, isPlainObject } from 'lodash-es'; + +// regex to match {{$keyword}} +const MOCK_PATTERN = /\{\{\$(\w+)\}\}/g; +const JSON_SPECIAL_CHARS = /[\\\n\r\t\"]/; + +const escapeJSONString = (str: string): string => { + if (!JSON_SPECIAL_CHARS.test(str)) { + return str; + } + + return str + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\"/g, '\\"'); +}; + +const prepareMock = (str: string, escapeJSONStrings: boolean): string => { + return str.replace(MOCK_PATTERN, (match, keyword) => { + let generatedValue = mockDataFunctions[keyword as keyof typeof mockDataFunctions]?.(); + + if (generatedValue === undefined) { + return match; + } + + generatedValue = String(generatedValue); + + return escapeJSONStrings ? escapeJSONString(generatedValue) : generatedValue; + }); +}; + +const prepareMockObj = ( + obj: Record, + escapeJSONStrings: boolean +): Record => { + const processed: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + if (typeof value === 'string') { + processed[key] = prepareMock(value, escapeJSONStrings); + } else if (isPlainObject(value)) { + // plain object is used to skip special objects like Date, RegExp, etc. + processed[key] = prepareMockObj(value, escapeJSONStrings); + } else { + processed[key] = value; + } + } + + return processed; +}; const interpolate = ( str: string, @@ -25,32 +76,14 @@ const interpolate = ( const { escapeJSONStrings } = options; - const patternRegex = /\{\{\$(\w+)\}\}/g; - str = str.replace(patternRegex, (match, keyword) => { - let replacement = mockDataFunctions[keyword as keyof typeof mockDataFunctions]?.(); - - if (replacement === undefined) return match; - replacement = String(replacement); - - if (!escapeJSONStrings) return replacement; - - // All the below chars inside of a JSON String field - // will make it invalid JSON. So we will have to escape them with `\`. - // This is not exhaustive but selective to what faker-js can output. - if (!/[\\\n\r\t\"]/.test(replacement)) return replacement; - return replacement - .replace(/\\/g, '\\\\') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t') - .replace(/\"/g, '\\"'); - }); + const preparedStr = prepareMock(str, escapeJSONStrings ?? false); if (!obj || typeof obj !== 'object') { - return str; + return preparedStr; } - - return replace(str, obj); + // process the object with the mock data functions + const preparedObj = prepareMockObj(obj, escapeJSONStrings ?? false); + return replace(preparedStr, preparedObj); }; const replace = (