Move mock builtin vars interpolation to bruno-common for CLI support (#4497)

* move interpolateMockVars function inside the main interpolate logic inside bruno-common.
* improve comments for JSON escaping logic in interpolate function
* update faker-functions to use CommonJS module syntax to satisfy jest and add regex validation tests
---------

Co-authored-by: sanjai0py <sanjailucifer666@gmail.com>
Co-authored-by: ramki-bruno <ramki@usebruno.com>
This commit is contained in:
Sanjai Kumar 2025-04-15 00:14:13 +05:30 committed by GitHub
parent 59e38fbdb0
commit d376947a91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 260 additions and 55 deletions

61
package-lock.json generated
View File

@ -26292,15 +26292,72 @@
"name": "@usebruno/common",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@faker-js/faker": "^9.7.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"rollup": "3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
"typescript": "^5.8.3"
}
},
"packages/bruno-common/node_modules/@faker-js/faker": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz",
"integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
},
"packages/bruno-common/node_modules/@rollup/plugin-typescript": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz",
"integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"packages/bruno-common/node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"packages/bruno-converters": {

View File

@ -32,7 +32,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
});
});
const _interpolate = (str) => {
const _interpolate = (str, { escapeJSONStrings } = {}) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
@ -51,7 +51,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
}
};
return interpolate(str, combinedVars);
return interpolate(str, combinedVars, { escapeJSONStrings });
};
request.url = _interpolate(request.url);
@ -67,14 +67,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
if (typeof request.data === 'object') {
try {
let parsed = JSON.stringify(request.data);
parsed = _interpolate(parsed);
parsed = _interpolate(parsed, { escapeJSONStrings: true });
request.data = JSON.parse(parsed);
} catch (err) {}
}
if (typeof request.data === 'string') {
if (request?.data?.length) {
request.data = _interpolate(request.data);
request.data = _interpolate(request.data, { escapeJSONStrings: true });
}
}
} else if (contentType === 'application/x-www-form-urlencoded') {

View File

@ -18,17 +18,20 @@
"build": "rollup -c",
"prepack": "npm run test && npm run build"
},
"dependencies": {
"@faker-js/faker": "^9.7.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
"typescript": "^5.8.3"
},
"overrides": {
"rollup":"3.29.5"
"rollup": "3.29.5"
}
}

View File

@ -31,10 +31,5 @@ module.exports = [
typescript({ tsconfig: './tsconfig.json' }),
terser()
]
},
{
input: 'dist/esm/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
plugins: [dts.default()]
}
];

View File

@ -11,16 +11,46 @@
* Output: Hello, my name is Bruno and I am 4 years old
*/
import { Set } from 'typescript';
import { flattenObject } from '../utils';
import { mockDataFunctions } from '../utils/faker-functions';
const interpolate = (str: string, obj: Record<string, any>): string => {
if (!str || typeof str !== 'string' || !obj || typeof obj !== 'object') {
const interpolate = (
str: string,
obj: Record<string, any>,
options: { escapeJSONStrings?: boolean } = { escapeJSONStrings: false }
): string => {
if (!str || typeof str !== 'string') {
return str;
}
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, '\\"');
});
if (!obj || typeof obj !== 'object') {
return str;
}
const flattenedObj = flattenObject(obj);
return replace(str, flattenedObj);
};

View File

@ -0,0 +1,141 @@
import { mockDataFunctions } from "./faker-functions";
describe("mockDataFunctions Regex Validation", () => {
test("all values should match their expected patterns", () => {
const patterns: Record<string, RegExp> = {
guid: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
timestamp: /^\d{13,}$/,
isoTimestamp: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
randomUUID: /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/,
randomAlphaNumeric: /^[\w]$/,
randomBoolean: /^(true|false)$/,
randomInt: /^\d+$/,
randomColor: /^[\w\s]+$/,
randomHexColor: /^#[\da-f]{6}$/,
randomAbbreviation: /^\w{2,6}$/,
randomIP: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$|^(\d{1,3}\.){3}\d{1,3}$/,
randomIPV4: /^(\d{1,3}\.){3}\d{1,3}$/,
randomIPV6: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/,
randomMACAddress: /^([\da-f]{2}:){5}[\da-f]{2}$/,
randomPassword: /^[\w\d]{8,}$/,
randomLocale: /^[A-Z]{2}$/,
randomUserAgent: /^[\w\/\.\s\(\)\+\-;:_,]+$/,
randomProtocol: /^(http|https|ftp)s?$/,
randomSemver: /^\d+\.\d+\.\d+$/,
randomFirstName: /^[\s\S]+$/,
randomLastName: /^[\s\S]+$/,
randomFullName: /^[\s\S]+$/,
randomNamePrefix: /^[\s\S]+$/,
randomNameSuffix: /^[\s\S]+$/,
randomJobArea: /^[\s\S]+$/,
randomJobDescriptor: /^[\s\S]+$/,
randomJobTitle: /^[\s\S]+$/,
randomJobType: /^[\s\S]+$/,
randomPhoneNumber: /^[\s\S]+$/,
randomPhoneNumberExt: /^[\s\S]+$/,
randomCity: /^[\s\S]+$/,
randomStreetName: /^[\s\S]+$/,
randomStreetAddress: /^[\s\S]+$/,
randomCountry: /^[\s\S]+$/,
randomCountryCode: /^[\s\S]+$/,
randomLatitude: /^[\s\S]+$/,
randomLongitude: /^[\s\S]+$/,
randomAvatarImage: /^[\s\S]+$/,
randomImageUrl: /^[\s\S]+$/,
randomAbstractImage: /^[\s\S]+$/,
randomAnimalsImage: /^[\s\S]+$/,
randomBusinessImage: /^[\s\S]+$/,
randomCatsImage: /^[\s\S]+$/,
randomCityImage: /^[\s\S]+$/,
randomFoodImage: /^[\s\S]+$/,
randomNightlifeImage: /^[\s\S]+$/,
randomFashionImage: /^[\s\S]+$/,
randomPeopleImage: /^[\s\S]+$/,
randomNatureImage: /^[\s\S]+$/,
randomSportsImage: /^[\s\S]+$/,
randomTransportImage: /^[\s\S]+$/,
randomImageDataUri: /^[\s\S]+$/,
randomBankAccount: /^[\s\S]+$/,
randomBankAccountName: /^[\s\S]+$/,
randomCreditCardMask: /^[\s\S]+$/,
randomBankAccountBic: /^[\s\S]+$/,
randomBankAccountIban: /^[\s\S]+$/,
randomTransactionType: /^[\s\S]+$/,
randomCurrencyCode: /^[\s\S]+$/,
randomCurrencyName: /^[\s\S]+$/,
randomCurrencySymbol: /^[\s\S]+$/,
randomBitcoin: /^[\s\S]+$/,
randomCompanyName: /^[\s\S]+$/,
randomCompanySuffix: /^[\s\S]+$/,
randomBs: /^[\s\S]+$/,
randomBsAdjective: /^[\s\S]+$/,
randomBsBuzz: /^[\s\S]+$/,
randomBsNoun: /^[\s\S]+$/,
randomCatchPhrase: /^[\s\S]+$/,
randomCatchPhraseAdjective: /^[\s\S]+$/,
randomCatchPhraseDescriptor: /^[\s\S]+$/,
randomCatchPhraseNoun: /^[\s\S]+$/,
randomDatabaseColumn: /^[\s\S]+$/,
randomDatabaseType: /^[\s\S]+$/,
randomDatabaseCollation: /^[\s\S]+$/,
randomDatabaseEngine: /^[\s\S]+$/,
randomDateFuture: /^[\s\S]+$/,
randomDatePast: /^[\s\S]+$/,
randomDateRecent: /^[\s\S]+$/,
randomWeekday: /^[\s\S]+$/,
randomMonth: /^[\s\S]+$/,
randomDomainName: /^[\s\S]+$/,
randomDomainSuffix: /^[\s\S]+$/,
randomDomainWord: /^[\s\S]+$/,
randomEmail: /^[\w_.\-]+@[\w]+\.[a-z]+$/,
randomExampleEmail: /^[\w\.-]+@example\.[a-z]+$/,
randomUserName: /^[\w.\-]+$/,
randomUrl: /^https:\/\/[\w\-]+\.[a-z]+\/?$/,
randomFileName: /^[\w\_]+\.[\w\d]+$/,
randomFileType: /^[\w]+$/,
randomFileExt: /^[\w\d]+$/,
randomCommonFileName: /^[\w\_]+\.[\w\d]+$/,
randomCommonFileType: /^[\w]+$/,
randomCommonFileExt: /^[\w\d]+$/,
randomFilePath: /^[\s\S]+$/,
randomDirectoryPath: /^\/[-\w\+\/]+$/,
randomMimeType: /^[\w]+\/[\w\d\-\+\.]+$/,
randomPrice: /^\d+\.\d{2}$/,
randomProduct: /^[\s\S]+$/,
randomProductAdjective: /^[\s\S]+$/,
randomProductMaterial: /^[\s\S]+$/,
randomProductName: /^[\s\S]+$/,
randomDepartment: /^[\s\S]+$/,
randomNoun: /^[\s\S]+$/,
randomVerb: /^[\s\S]+$/,
randomIngverb: /^[\s\S]+$/,
randomAdjective: /^[\s\S]+$/,
randomWord: /^[\s\S]+$/,
randomWords: /^[\s\S]+$/,
randomPhrase: /^[\s\S]+$/,
randomLoremWord: /^[\s\S]+$/,
randomLoremWords: /^[\s\S]+$/,
randomLoremSentence: /^[\s\S]+$/,
randomLoremSentences: /^[\s\S]+$/,
randomLoremParagraph: /^[\s\S]+$/,
randomLoremParagraphs: /^[\s\S]+$/,
randomLoremText: /^[\s\S]+$/,
randomLoremSlug: /^[\s\S]+$/,
randomLoremLines: /^[\s\S]+$/,
};
const errors: string[] = [];
Object.entries(mockDataFunctions).forEach(([key, func]) => {
const pattern = patterns[key];
const value = String(func());
if (!value.match(pattern)) {
errors.push(`Pattern mismatch for ${key}: expected ${pattern}, received ${value}`);
}
});
if (errors.length > 0) {
throw new Error(errors.join("\n"));
}
});
});

View File

@ -1,6 +1,6 @@
const { faker } = require('@faker-js/faker');
import { faker } from '@faker-js/faker';
const mockDataFunctions = {
export const mockDataFunctions = {
guid: () => faker.string.uuid(),
timestamp: () => faker.date.anytime().getTime().toString(),
isoTimestamp: () => faker.date.anytime().toISOString(),
@ -9,7 +9,7 @@ const mockDataFunctions = {
randomBoolean: () => faker.datatype.boolean(),
randomInt: () => faker.number.int(),
randomColor: () => faker.color.human(),
randomHexColor: () => faker.internet.color(),
randomHexColor: () => faker.color.rgb(),
randomAbbreviation: () => faker.hacker.abbreviation(),
randomIP: () => faker.internet.ip(),
randomIPV4: () => faker.internet.ipv4(),
@ -121,7 +121,3 @@ const mockDataFunctions = {
randomLoremSlug: () => faker.lorem.slug(),
randomLoremLines: () => faker.lorem.lines()
};
module.exports = {
mockDataFunctions
};

View File

@ -6,14 +6,14 @@
"skipLibCheck": true,
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"checkJs": false
},
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["dist", "node_modules", "tests"]
}

View File

@ -1,7 +1,6 @@
const { interpolate } = require('@usebruno/common');
const { each, forOwn, cloneDeep, find } = require('lodash');
const FormData = require('form-data');
const { mockDataFunctions } = require('./faker-functions');
const getContentType = (headers = {}) => {
let contentType = '';
@ -14,28 +13,6 @@ const getContentType = (headers = {}) => {
return contentType;
};
const interpolateMockVars = (str, { escapeJSONStrings }) => {
const patternRegex = /\{\{\$(\w+)\}\}/g;
return str.replace(patternRegex, (match, keyword) => {
let replacement = mockDataFunctions[keyword]?.();
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 interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
@ -78,7 +55,9 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
}
};
return interpolateMockVars(interpolate(str, combinedVars), { escapeJSONStrings });
return interpolate(str, combinedVars, {
escapeJSONStrings
});
};
request.url = _interpolate(request.url);
@ -97,12 +76,16 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data, { escapeJSONStrings: true });
request.data = _interpolate(request.data, {
escapeJSONStrings: true
});
}
} else if (typeof request.data === 'object') {
try {
const jsonDoc = JSON.stringify(request.data);
const parsed = _interpolate(jsonDoc, { escapeJSONStrings: true });
const parsed = _interpolate(jsonDoc, {
escapeJSONStrings: true
});
request.data = JSON.parse(parsed);
} catch (err) {}
}