mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 14:44:07 +00:00
fix: enhance URL parameter parsing and interpolation logic (#5812)
* fix: enhance URL parameter parsing and interpolation logic
This commit is contained in:
@@ -33,19 +33,35 @@ export const parsePathParams = (url) => {
|
||||
}
|
||||
|
||||
// Enhanced: also match :param inside parentheses and/or quotes
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
const foundParams = new Set();
|
||||
paths.forEach(segment => {
|
||||
// traditional path parameters
|
||||
if (segment.startsWith(':')) {
|
||||
const name = segment.slice(1);
|
||||
if (name && !foundParams.has(name)) {
|
||||
foundParams.add(name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// for OData-style parameters (parameters inside parentheses)
|
||||
// Check if segment matches valid OData syntax:
|
||||
// 1. EntitySet('key') or EntitySet(key)
|
||||
// 2. EntitySet(Key1=value1,Key2=value2)
|
||||
// 3. Function(param=value)
|
||||
if (!/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(segment)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
let match;
|
||||
while ((match = paramRegex.exec(segment))) {
|
||||
if (match[1]) {
|
||||
// Clean up: remove trailing quotes/parentheses if present
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
// Remove leading quotes/parentheses if present
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (name && !foundParams.has(name)) {
|
||||
foundParams.add(name);
|
||||
}
|
||||
if (!match[1]) continue;
|
||||
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (name && !foundParams.has(name)) {
|
||||
foundParams.add(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -84,27 +100,41 @@ export const interpolateUrl = ({ url, variables }) => {
|
||||
|
||||
export const interpolateUrlPathParams = (url, params) => {
|
||||
const getInterpolatedBasePath = (pathname, params) => {
|
||||
const regex = /[:](\w+)/g;
|
||||
return pathname
|
||||
.split('/')
|
||||
.map((segment) => {
|
||||
// traditional path parameters
|
||||
if (segment.startsWith(':')) {
|
||||
const name = segment.slice(1);
|
||||
const pathParam = params.find((p) => p?.name === name && p?.type === 'path');
|
||||
return pathParam ? pathParam.value : segment;
|
||||
}
|
||||
|
||||
if (!segment.startsWith(':')) return segment;
|
||||
// for OData-style parameters (parameters inside parentheses)
|
||||
// Check if segment matches valid OData syntax:
|
||||
// 1. EntitySet('key') or EntitySet(key)
|
||||
// 2. EntitySet(Key1=value1,Key2=value2)
|
||||
// 3. Function(param=value)
|
||||
if (!/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(segment)) {
|
||||
return segment;
|
||||
}
|
||||
|
||||
const regex = /[:](\w+)/g;
|
||||
let match;
|
||||
let result = segment;
|
||||
while ((match = regex.exec(segment))) {
|
||||
if (match[1]) {
|
||||
// Clean up: remove trailing quotes/parentheses if present
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
// Remove leading quotes/parentheses if present
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (name) {
|
||||
const pathParam = params.find(p => p?.name === name && p?.type === 'path');
|
||||
return pathParam ? pathParam.value : segment;
|
||||
}
|
||||
if (!match[1]) continue;
|
||||
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (!name) continue;
|
||||
|
||||
const pathParam = params.find((p) => p?.name === name && p?.type === 'path');
|
||||
if (pathParam) {
|
||||
result = result.replace(':' + match[1], pathParam.value);
|
||||
}
|
||||
}
|
||||
return segment;
|
||||
return result;
|
||||
})
|
||||
.join('/');
|
||||
};
|
||||
|
||||
@@ -80,13 +80,19 @@ describe('Url Utils - parsePathParams', () => {
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with mixed quote types', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")');
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse OData entity key with parentheses only', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products(:productId)');
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse OData composite key with multiple parameters', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=\':productId\')');
|
||||
it('should parse OData composite key with mixed parameter styles', () => {
|
||||
// Test both positional and named parameter styles in the same key
|
||||
const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=:productId)');
|
||||
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);
|
||||
});
|
||||
|
||||
@@ -115,19 +121,24 @@ describe('Url Utils - parsePathParams', () => {
|
||||
expect(params).toEqual([{ name: 'product_id', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with mixed quote types', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products(\':productId\')/Categories(":categoryId")');
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]);
|
||||
it('should parse OData composite key with positional parameters', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Orders(:orderId,:productId)');
|
||||
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with nested parentheses', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products((\':productId\'))');
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }]);
|
||||
it('should parse OData composite key with named parameters', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Orders(OrderId=:orderId,ProductId=:productId)');
|
||||
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with complex nested structures', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\':itemId\')/Properties(\':propName\')');
|
||||
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);
|
||||
it('should handle OData navigation properties', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items');
|
||||
expect(params).toEqual([{ name: 'orderId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData function parameters', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products/GetProductsByCategory(categoryId=:categoryId)');
|
||||
expect(params).toEqual([{ name: 'categoryId', value: '' }]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with query options in path', () => {
|
||||
@@ -145,9 +156,15 @@ describe('Url Utils - parsePathParams', () => {
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with function calls in parentheses', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products(GetId(\':productId\'))');
|
||||
expect(params).toEqual([{ name: 'productId', value: '' }]);
|
||||
it('should NOT treat embedded colons as path parameters (regression fix)', () => {
|
||||
// This test case reproduces the bug reported in issue #5805
|
||||
const params = parsePathParams('/start/1:2:AHLS-HASD/form');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should NOT treat embedded colons as path parameters in full URLs', () => {
|
||||
const params = parsePathParams('https://example.com/start/1:2:AHLS-HASD/form');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle OData parameters with escaped quotes', () => {
|
||||
@@ -169,6 +186,11 @@ describe('Url Utils - parsePathParams', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products(\'123e4567-e89b-12d3-a456-426614174000\')');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle OData with query parameters for variable interpolation', () => {
|
||||
const params = parsePathParams('https://example.com/odata/Products?$filter=Category eq \'{{category}}\'&$orderby={{sortField}}');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - splitOnFirst', () => {
|
||||
|
||||
@@ -116,22 +116,44 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
throw { message: 'Invalid URL format', originalError: e.message };
|
||||
}
|
||||
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
const interpolatedUrlPath = url.pathname
|
||||
.split('/')
|
||||
.filter((path) => path !== '')
|
||||
.map((path) => {
|
||||
const matches = path.match(paramRegex);
|
||||
if (matches) {
|
||||
const paramName = matches[0].slice(1); // Remove the : prefix
|
||||
// traditional path parameters
|
||||
if (path.startsWith(':')) {
|
||||
const paramName = path.slice(1);
|
||||
const existingPathParam = request.pathParams.find(param => param.name === paramName);
|
||||
if (!existingPathParam) {
|
||||
return '/' + path;
|
||||
}
|
||||
return '/' + path.replace(':' + paramName, existingPathParam.value);
|
||||
} else {
|
||||
return '/' + path;
|
||||
return '/' + existingPathParam.value;
|
||||
}
|
||||
|
||||
// for OData-style parameters (parameters inside parentheses)
|
||||
// Check if path matches valid OData syntax:
|
||||
// 1. EntitySet('key') or EntitySet(key)
|
||||
// 2. EntitySet(Key1=value1,Key2=value2)
|
||||
// 3. Function(param=value)
|
||||
if (/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(path)) {
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
let match;
|
||||
let result = path;
|
||||
while ((match = paramRegex.exec(path))) {
|
||||
if (match[1]) {
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (name) {
|
||||
const existingPathParam = request.pathParams.find((param) => param.name === name);
|
||||
if (existingPathParam) {
|
||||
result = result.replace(':' + match[1], existingPathParam.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '/' + result;
|
||||
}
|
||||
return '/' + path;
|
||||
})
|
||||
.join('');
|
||||
|
||||
|
||||
@@ -141,22 +141,44 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
throw { message: 'Invalid URL format', originalError: e.message };
|
||||
}
|
||||
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
const urlPathnameInterpolatedWithPathParams = url.pathname
|
||||
.split('/')
|
||||
.filter((path) => path !== '')
|
||||
.map((path) => {
|
||||
const matches = path.match(paramRegex);
|
||||
if (matches) {
|
||||
const paramName = matches[0].slice(1); // Remove the : prefix
|
||||
// traditional path parameters
|
||||
if (path.startsWith(':')) {
|
||||
const paramName = path.slice(1);
|
||||
const existingPathParam = request.pathParams.find(param => param.name === paramName);
|
||||
if (!existingPathParam) {
|
||||
return '/' + path;
|
||||
}
|
||||
return '/' + path.replace(':' + paramName, existingPathParam.value);
|
||||
} else {
|
||||
return '/' + path;
|
||||
return '/' + existingPathParam.value;
|
||||
}
|
||||
|
||||
// for OData-style parameters (parameters inside parentheses)
|
||||
// Check if path matches valid OData syntax:
|
||||
// 1. EntitySet('key') or EntitySet(key)
|
||||
// 2. EntitySet(Key1=value1,Key2=value2)
|
||||
// 3. Function(param=value)
|
||||
if (/^[A-Za-z0-9_.-]+\([^)]*\)$/.test(path)) {
|
||||
const paramRegex = /[:](\w+)/g;
|
||||
let match;
|
||||
let result = path;
|
||||
while ((match = paramRegex.exec(path))) {
|
||||
if (match[1]) {
|
||||
let name = match[1].replace(/[')"`]+$/, '');
|
||||
name = name.replace(/^[('"`]+/, '');
|
||||
if (name) {
|
||||
const existingPathParam = request.pathParams.find((param) => param.name === name);
|
||||
if (existingPathParam) {
|
||||
result = result.replace(':' + match[1], existingPathParam.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '/' + result;
|
||||
}
|
||||
return '/' + path;
|
||||
})
|
||||
.join('');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user