Skip to content

Enable variable tootlip in json request body #4885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/bruno-app/src/components/CodeEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,17 @@ export default class CodeEditor extends React.Component {
}

componentDidMount() {
const variables = getAllVariables(this.props.collection, this.props.item);

const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
lineNumbers: true,
lineWrapping: true,
tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json',
brunoVarInfo: {
variables
},
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
Expand Down
47 changes: 30 additions & 17 deletions packages/bruno-app/src/utils/codemirror/brunoVarInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,8 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');

const renderVarInfo = (token, options, cm, pos) => {
const str = token.string || '';
if (!str || !str.length || typeof str !== 'string') {
return;
}

// str is of format {{variableName}} or :variableName, extract variableName
let variableName;
let variableValue;

if (str.startsWith('{{')) {
variableName = str.replace('{{', '').replace('}}', '').trim();
variableValue = interpolate(get(options.variables, variableName), options.variables);
} else if (str.startsWith('/:')) {
variableName = str.replace('/:', '').trim();
variableValue =
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
}
// Extract variable name and value based on token
const { variableName, variableValue } = extractVariableInfo(token.string, options.variables);

if (variableValue === undefined) {
return;
Expand All @@ -41,11 +26,13 @@ if (!SERVER_RENDERED) {
const into = document.createElement('div');
const descriptionDiv = document.createElement('div');
descriptionDiv.className = 'info-description';

if (options?.variables?.maskedEnvVariables?.includes(variableName)) {
descriptionDiv.appendChild(document.createTextNode('*****'));
} else {
descriptionDiv.appendChild(document.createTextNode(variableValue));
}

into.appendChild(descriptionDiv);

return into;
Expand Down Expand Up @@ -202,3 +189,29 @@ if (!SERVER_RENDERED) {
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
}
}

export const extractVariableInfo = (str, variables) => {
let variableName;
let variableValue;

if (!str || !str.length || typeof str !== 'string') {
return { variableName, variableValue };
}

// Regex to match double brace variable syntax: {{variableName}}
const DOUBLE_BRACE_PATTERN = /\{\{([^}]+)\}\}/;

if (DOUBLE_BRACE_PATTERN.test(str)) {
variableName = str.replace('{{', '').replace('}}', '').trim();
variableValue = interpolate(get(variables, variableName), variables);
} else if (str.startsWith('/:')) {
variableName = str.replace('/:', '').trim();
variableValue = variables?.pathParams?.[variableName];
} else {
// direct variable reference (e.g., for numeric values in JSON mode or plain variable names)
variableName = str;
variableValue = interpolate(get(variables, variableName), variables);
}

return { variableName, variableValue };
};
227 changes: 227 additions & 0 deletions packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { interpolate } from '@usebruno/common';
import { extractVariableInfo } from './brunoVarInfo';

// Mock the dependencies
jest.mock('@usebruno/common', () => ({
interpolate: jest.fn()
}));

describe('extractVariableInfo', () => {
let mockVariables;

beforeEach(() => {
jest.clearAllMocks();

// Setup mock variables
mockVariables = {
apiKey: 'test-api-key-123',
baseUrl: 'https://api.example.com',
userId: 12345,
pathParams: {
id: 'user-123',
slug: 'test-post'
}
};

// Setup interpolate mock
interpolate.mockImplementation((value, variables) => {
if (typeof value === 'string' && value.includes('{{')) {
return value.replace(/\{\{(\w+)\}\}/g, (match, key) => variables[key] || match);
}
return value;
});
});

describe('input validation', () => {
it('should return undefined for null input', () => {
const result = extractVariableInfo(null, mockVariables);
expect(result.variableName).toBeUndefined();
expect(result.variableValue).toBeUndefined();
});

it('should return undefined for undefined input', () => {
const result = extractVariableInfo(undefined, mockVariables);
expect(result.variableName).toBeUndefined();
expect(result.variableValue).toBeUndefined();
});

it('should return undefined for empty string', () => {
const result = extractVariableInfo('', mockVariables);
expect(result.variableName).toBeUndefined();
expect(result.variableValue).toBeUndefined();
});

it('should return undefined for non-string input', () => {
const result = extractVariableInfo(123, mockVariables);
expect(result.variableName).toBeUndefined();
expect(result.variableValue).toBeUndefined();
});

it('should return undefined for object input', () => {
const result = extractVariableInfo({}, mockVariables);
expect(result.variableName).toBeUndefined();
expect(result.variableValue).toBeUndefined();
});
});

describe('double brace format ({{variableName}})', () => {
it('should parse double brace variables correctly', () => {
const result = extractVariableInfo('{{apiKey}}', mockVariables);

expect(result).toEqual({
variableName: 'apiKey',
variableValue: 'test-api-key-123'
});

expect(interpolate).toHaveBeenCalledWith('test-api-key-123', mockVariables);
});

it('should handle whitespace in double brace variables', () => {
const result = extractVariableInfo('{{ apiKey }}', mockVariables);

expect(result).toEqual({
variableName: 'apiKey',
variableValue: 'test-api-key-123'
});
});

it('should return undefined variableValue for non-existent double brace variable', () => {
const result = extractVariableInfo('{{nonExistent}}', mockVariables);

expect(result).toEqual({
variableName: 'nonExistent',
variableValue: undefined
});
});
});

describe('path parameter format (/:variableName)', () => {
it('should parse path parameter variables correctly', () => {
const result = extractVariableInfo('/:id', mockVariables);

expect(result).toEqual({
variableName: 'id',
variableValue: 'user-123'
});
});

it('should return undefined for non-existent path parameter', () => {
const result = extractVariableInfo('/:nonExistent', mockVariables);

expect(result).toEqual({
variableName: 'nonExistent',
variableValue: undefined
});
});

it('should handle missing pathParams object', () => {
const variablesWithoutPathParams = { ...mockVariables };
delete variablesWithoutPathParams.pathParams;

const result = extractVariableInfo('/:id', variablesWithoutPathParams);

expect(result).toEqual({
variableName: 'id',
variableValue: undefined
});
});

it('should handle null pathParams', () => {
const variablesWithNullPathParams = { ...mockVariables, pathParams: null };

const result = extractVariableInfo('/:id', variablesWithNullPathParams);

expect(result).toEqual({
variableName: 'id',
variableValue: undefined
});
});
});

describe('direct variable format', () => {
it('should parse direct variable names correctly', () => {
const result = extractVariableInfo('baseUrl', mockVariables);

expect(result).toEqual({
variableName: 'baseUrl',
variableValue: 'https://api.example.com'
});

expect(interpolate).toHaveBeenCalledWith('https://api.example.com', mockVariables);
});

it('should handle numeric variable values', () => {
const result = extractVariableInfo('userId', mockVariables);

expect(result).toEqual({
variableName: 'userId',
variableValue: 12345
});
});

it('should return undefined for non-existent direct variable', () => {
const result = extractVariableInfo('nonExistent', mockVariables);

expect(result).toEqual({
variableName: 'nonExistent',
variableValue: undefined
});
});

it('should handle variables with special characters', () => {
mockVariables['special-var_name'] = 'special-var_value';

const result = extractVariableInfo('special-var_name', mockVariables);

expect(result).toEqual({
variableName: 'special-var_name',
variableValue: 'special-var_value'
});
});
});

describe('edge cases', () => {
it('should handle empty variables object', () => {
const result = extractVariableInfo('{{apiKey}}', {});

expect(result).toEqual({
variableName: 'apiKey',
variableValue: undefined
});
});

it('should handle null variables object', () => {
const result = extractVariableInfo('{{apiKey}}', null);

expect(result).toEqual({
variableName: 'apiKey',
variableValue: undefined
});
});

it('should handle undefined variables object', () => {
const result = extractVariableInfo('{{apiKey}}', undefined);

expect(result).toEqual({
variableName: 'apiKey',
variableValue: undefined
});
});
});

describe('return value structure', () => {
it('should always return an object with variableName and variableValue properties', () => {
const result = extractVariableInfo('{{apiKey}}', mockVariables);

expect(result).toHaveProperty('variableName');
expect(result).toHaveProperty('variableValue');
expect(typeof result.variableName).toBe('string');
});

it('should return variableValue as the interpolated value', () => {
const result = extractVariableInfo('{{apiKey}}', mockVariables);

expect(result.variableValue).toBe('test-api-key-123');
});
});
});
Loading