From 40b44de294ab3117a29bf54d00851150fd16fb42 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 23 Sep 2025 09:01:00 +0200 Subject: [PATCH] Support for Odata style path params (#5048) * Support for Odata style path params * Remove test statement * Update packages/bruno-app/src/utils/url/index.spec.js Add more testcases Co-authored-by: sid-bruno * Performance improvements * Add testcases for odata style url params --------- Co-authored-by: sid-bruno --- packages/bruno-app/src/utils/url/index.js | 45 +++++-- .../bruno-app/src/utils/url/index.spec.js | 126 ++++++++++++++++++ .../bruno-cli/src/runner/interpolate-vars.js | 15 ++- .../src/ipc/network/interpolate-vars.js | 23 ++-- 4 files changed, 181 insertions(+), 28 deletions(-) diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index a8ac1b812..ae8d10c11 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -32,19 +32,24 @@ export const parsePathParams = (url) => { paths = uri.split('/'); } - paths = paths.reduce((acc, path) => { - if (path !== '' && path[0] === ':') { - let name = path.slice(1, path.length); - if (name) { - let isExist = find(acc, (path) => path.name === name); - if (!isExist) { - acc.push({ name: path.slice(1, path.length), value: '' }); + // Enhanced: also match :param inside parentheses and/or quotes + const paramRegex = /[:](\w+)/g; + const foundParams = new Set(); + paths.forEach((segment) => { + 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); } } } - return acc; - }, []); - return paths; + }); + return Array.from(foundParams).map((name) => ({ name, value: '' })); }; export const splitOnFirst = (str, char) => { @@ -79,13 +84,25 @@ export const interpolateUrl = ({ url, variables }) => { export const interpolateUrlPathParams = (url, params) => { const getInterpolatedBasePath = (pathname, params) => { + const regex = /[:](\w+)/g; return pathname .split('/') .map((segment) => { - if (segment.startsWith(':')) { - const pathParamName = segment.slice(1); - const pathParam = params.find((p) => p?.name === pathParamName && p?.type === 'path'); - return pathParam ? pathParam.value : segment; + + if(!segment.startsWith(":")) return segment + + let match; + 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; + } + } } return segment; }) diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index bbcc919c8..c75ce63c8 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -43,6 +43,132 @@ describe('Url Utils - parsePathParams', () => { { name: 'postId', value: '' } ]); }); + + it('should parse path param inside parentheses and quotes', () => { + const params = parsePathParams("https://example.com/ExchangeRates(':ExchangeRateOID')"); + expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse path param inside parentheses and no quotes', () => { + const params = parsePathParams("https://example.com/ExchangeRates(:ExchangeRateOID)"); + expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse multiple path params inside parentheses', () => { + const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)"); + expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'ExchangeRateOID', value: '' }]); + }); + + it('should parse mix and match of normal and param inside parentheses', () => { + const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/:key"); + expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'key', value: '' }]); + }); + + // OData-specific test cases for enhanced path parameter parsing + it('should parse OData entity key with single quotes', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData entity key with double quotes', () => { + const params = parsePathParams('https://example.com/odata/Products(":productId")'); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should parse OData entity key with backticks', () => { + const params = parsePathParams('https://example.com/odata/Products(`:productId`)'); + expect(params).toEqual([{ name: 'productId', 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')"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]); + }); + + it('should parse OData navigation property with key', () => { + const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')"); + expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]); + }); + + it('should parse OData function with parameters', () => { + const params = parsePathParams("https://example.com/odata/GetProductsByCategory(categoryId=':categoryId')"); + expect(params).toEqual([{ name: 'categoryId', value: '' }]); + }); + + it('should parse OData action with complex parameters', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')/Rate(rating=:rating,comment=':comment')"); + expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]); + }); + + it('should handle OData parameters with special characters in names', () => { + const params = parsePathParams("https://example.com/odata/Products(':product-id')"); + expect(params).toEqual([{ name: 'product', value: '' }]); + }); + + it('should handle OData parameters with underscores in names', () => { + const params = parsePathParams("https://example.com/odata/Products(':product_id')"); + 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 handle OData parameters with nested parentheses', () => { + const params = parsePathParams("https://example.com/odata/Products((':productId'))"); + expect(params).toEqual([{ 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 parameters with query options in path', () => { + const params = parsePathParams("https://example.com/odata/Products(':productId')?$expand=Category"); + expect(params).toEqual([{ name: 'productId', value: '' }]); + }); + + it('should handle OData parameters with multiple segments and mixed syntax', () => { + 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 parameters with empty string values', () => { + const params = parsePathParams("https://example.com/odata/Products('')"); + 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 handle OData parameters with escaped quotes', () => { + const params = parsePathParams("https://example.com/odata/Products('ABC''123')"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with spaces in quotes', () => { + const params = parsePathParams("https://example.com/odata/Products('Product Name With Spaces')"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with numeric keys', () => { + const params = parsePathParams("https://example.com/odata/Products(12345)"); + expect(params).toEqual([]); + }); + + it('should handle OData parameters with GUID keys', () => { + const params = parsePathParams("https://example.com/odata/Products('123e4567-e89b-12d3-a456-426614174000')"); + expect(params).toEqual([]); + }); }); describe('Url Utils - splitOnFirst', () => { diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 7ec7041b5..83b9e0881 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -115,16 +115,21 @@ 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) => { - if (path[0] !== ':') { - return '/' + path; + const matches = path.match(paramRegex); + if (matches) { + const paramName = matches[0].slice(1); // Remove the : prefix + const existingPathParam = request.pathParams.find(param => param.name === paramName); + if (!existingPathParam) { + return '/' + path; + } + return '/' + path.replace(':' + paramName, existingPathParam.value); } else { - const name = path.slice(1); - const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name); - return existingPathParam ? '/' + existingPathParam.value : ''; + return '/' + path; } }) .join(''); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 0aff18f19..7ab2dea61 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -91,15 +91,15 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc if (typeof request.data === 'string') { if (request.data.length) { request.data = _interpolate(request.data, { - escapeJSONStrings: true - }); + escapeJSONStrings: true + }); } } else if (typeof request.data === 'object') { try { const jsonDoc = JSON.stringify(request.data); const parsed = _interpolate(jsonDoc, { - escapeJSONStrings: true - }); + escapeJSONStrings: true + }); request.data = JSON.parse(parsed); } catch (err) {} } @@ -142,16 +142,21 @@ 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) => { - if (path[0] !== ':') { - return '/' + path; + const matches = path.match(paramRegex); + if (matches) { + const paramName = matches[0].slice(1); // Remove the : prefix + const existingPathParam = request.pathParams.find(param => param.name === paramName); + if (!existingPathParam) { + return '/' + path; + } + return '/' + path.replace(':' + paramName, existingPathParam.value); } else { - const name = path.slice(1); - const existingPathParam = request.pathParams.find((param) => param.type === 'path' && param.name === name); - return existingPathParam ? '/' + existingPathParam.value : ''; + return '/' + path; } }) .join('');