diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index 73d7d0890b..6e1bc9e933 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -33,8 +33,9 @@ const transformOpenapiRequestItem = (request) => { operationName = `${request.method} ${request.path}`; } - // replace OpenAPI links in path by Bruno variables - let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`); + + // Replace {param} with :param in the path + let path = request.path.replace(/{([a-zA-Z]+)}/g, ':$1'); const brunoRequestItem = { uid: uuid(), @@ -70,7 +71,7 @@ const transformOpenapiRequestItem = (request) => { brunoRequestItem.request.params.push({ uid: uuid(), name: param.name, - value: '', + value: param.schema?.default?.toString() || '', description: param.description || '', enabled: param.required, type: 'query' @@ -79,7 +80,7 @@ const transformOpenapiRequestItem = (request) => { brunoRequestItem.request.params.push({ uid: uuid(), name: param.name, - value: '', + value: param.schema?.default?.toString() || '', description: param.description || '', enabled: param.required, type: 'path' @@ -88,7 +89,7 @@ const transformOpenapiRequestItem = (request) => { brunoRequestItem.request.headers.push({ uid: uuid(), name: param.name, - value: '', + value: param.schema?.default?.toString() || '', description: param.description || '', enabled: param.required }); @@ -251,35 +252,84 @@ const resolveRefs = (spec, components = spec?.components, cache = new Map()) => return resolved; }; -const groupRequestsByTags = (requests) => { - let _groups = {}; - let ungrouped = []; - each(requests, (request) => { - let tags = request.operationObject.tags || []; - if (tags.length > 0) { - let tag = tags[0].trim(); // take first tag and trim whitespace - - if (tag) { - if (!_groups[tag]) { - _groups[tag] = []; - } - _groups[tag].push(request); - } else { - ungrouped.push(request); +const groupRequestsByPath = (requests) => { + const pathGroups = {}; + + requests.forEach((request) => { + const pathSegments = request.path.split('/').filter(Boolean); + let currentPath = ''; + let currentGroup = pathGroups; + + pathSegments.forEach((segment, index) => { + if (segment.startsWith(':')) { + // Skip path parameters for folder names + return; } - } else { - ungrouped.push(request); + + currentPath = currentPath ? `${currentPath}/${segment}` : segment; + + if (!currentGroup[currentPath]) { + currentGroup[currentPath] = { + name: segment, + path: currentPath, + items: [], + subGroups: {} + }; + } + + if (index === pathSegments.length - 1) { + currentGroup[currentPath].items.push(request); + } + + currentGroup = currentGroup[currentPath].subGroups; + }); + + // If no valid segments (e.g., only path parameters), add to root + if (!pathSegments.some(segment => !segment.startsWith(':'))) { + if (!pathGroups.root) { + pathGroups.root = { + name: 'root', + path: '', + items: [], + subGroups: {} + }; + } + pathGroups.root.items.push(request); } }); - let groups = Object.keys(_groups).map((groupName) => { - return { - name: groupName, - requests: _groups[groupName] - }; - }); + const buildFolderStructure = (group) => { + // Transform request items + const items = group.items.map(transformOpenapiRequestItem); + + // Process subfolders + const subFolders = []; + Object.values(group.subGroups).forEach(subGroup => { + // Process each subfolder recursively + const subFolderItems = buildFolderStructure(subGroup); + + // Only create a folder if it has items + if (subFolderItems.length > 0) { + subFolders.push({ + uid: uuid(), + name: subGroup.name, + type: 'folder', + items: subFolderItems + }); + } + }); - return [groups, ungrouped]; + return [...items, ...subFolders]; + }; + + const folders = Object.values(pathGroups).map(group => ({ + uid: uuid(), + name: group.name, + type: 'folder', + items: buildFolderStructure(group) + })); + + return folders; }; const getDefaultUrl = (serverObject) => { @@ -389,10 +439,10 @@ export const parseOpenApiCollection = (data) => { .map(([method, operationObject]) => { return { method: method, - path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons + path: path, operationObject: operationObject, global: { - server: '{{baseUrl}}', + server: '{{baseUrl}}', security: securityConfig } }; @@ -400,19 +450,7 @@ export const parseOpenApiCollection = (data) => { }) .reduce((acc, val) => acc.concat(val), []); // flatten - let [groups, ungroupedRequests] = groupRequestsByTags(allRequests); - let brunoFolders = groups.map((group) => { - return { - uid: uuid(), - name: group.name, - type: 'folder', - items: group.requests.map(transformOpenapiRequestItem) - }; - }); - - let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem); - let brunoCollectionItems = brunoFolders.concat(ungroupedItems); - brunoCollection.items = brunoCollectionItems; + brunoCollection.items = groupRequestsByPath(allRequests); return brunoCollection; } catch (err) { console.error(err); diff --git a/packages/bruno-converters/tests/openapi/openapi-path-grouping.spec.js b/packages/bruno-converters/tests/openapi/openapi-path-grouping.spec.js new file mode 100644 index 0000000000..ebe6bce3b5 --- /dev/null +++ b/packages/bruno-converters/tests/openapi/openapi-path-grouping.spec.js @@ -0,0 +1,243 @@ +import jsyaml from 'js-yaml'; +import { describe, it, expect } from '@jest/globals'; +import openApiToBruno from '../../src/openapi/openapi-to-bruno'; + +describe('openapi-path-based-grouping', () => { + it('should correctly group endpoints by path segments', async () => { + const openApiSpecification = jsyaml.load(openApiWithNestedPaths); + const brunoCollection = openApiToBruno(openApiSpecification); + + expect(brunoCollection).toMatchObject(expectedNestedPathsOutput); + }); +}); + +const openApiWithNestedPaths = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Path Grouping Test API" +paths: + /api/users: + get: + summary: "List Users" + responses: + '200': + description: "Successful response" + post: + summary: "Create User" + responses: + '201': + description: "User created" + /api/users/{userId}: + get: + summary: "Get User" + parameters: + - name: userId + in: path + required: true + schema: + type: string + responses: + '200': + description: "User found" + /api/users/{userId}/posts: + get: + summary: "List User Posts" + parameters: + - name: userId + in: path + required: true + schema: + type: string + responses: + '200': + description: "Posts found" +servers: + - url: "https://api.example.com" +`; + +const expectedNestedPathsOutput = { + "name": "Path Grouping Test API", + "uid": "mockeduuidvalue123456", + "version": "1", + "environments": [ + { + "name": "Environment 1", + "uid": "mockeduuidvalue123456", + "variables": [ + { + "enabled": true, + "name": "baseUrl", + "secret": false, + "type": "text", + "uid": "mockeduuidvalue123456", + "value": "https://api.example.com" + } + ] + } + ], + "items": [ + { + "name": "api", + "type": "folder", + "uid": "mockeduuidvalue123456", + "items": [ + { + "name": "users", + "type": "folder", + "uid": "mockeduuidvalue123456", + "items": [ + { + "name": "List Users", + "type": "http-request", + "uid": "mockeduuidvalue123456", + "request": { + "auth": { + "basic": null, + "bearer": null, + "digest": null, + "mode": "none" + }, + "body": { + "formUrlEncoded": [], + "json": null, + "mode": "none", + "multipartForm": [], + "text": null, + "xml": null + }, + "headers": [], + "method": "GET", + "params": [], + "script": { + "res": null + }, + "url": "{{baseUrl}}/api/users" + }, + "seq": 1 + }, + { + "name": "Create User", + "type": "http-request", + "uid": "mockeduuidvalue123456", + "request": { + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "digest": null + }, + "body": { + "mode": "none", + "json": null, + "text": null, + "xml": null, + "formUrlEncoded": [], + "multipartForm": [] + }, + "headers": [], + "method": "POST", + "params": [], + "script": { + "res": null + }, + "url": "{{baseUrl}}/api/users" + }, + "seq": 2 + }, + { + "name": "{userId}", + "type": "folder", + "uid": "mockeduuidvalue123456", + "items": [ + { + "name": "Get User", + "type": "http-request", + "uid": "mockeduuidvalue123456", + "request": { + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "digest": null + }, + "body": { + "mode": "none", + "json": null, + "text": null, + "xml": null, + "formUrlEncoded": [], + "multipartForm": [] + }, + "headers": [], + "method": "GET", + "params": [ + { + "name": "userId", + "value": "", + "description": "", + "enabled": true, + "type": "path", + "uid": "mockeduuidvalue123456" + } + ], + "script": { + "res": null + }, + "url": "{{baseUrl}}/api/users/:userId" + }, + "seq": 1 + }, + { + "name": "posts", + "type": "folder", + "uid": "mockeduuidvalue123456", + "items": [ + { + "name": "List User Posts", + "type": "http-request", + "uid": "mockeduuidvalue123456", + "request": { + "auth": { + "mode": "none", + "basic": null, + "bearer": null, + "digest": null + }, + "body": { + "mode": "none", + "json": null, + "text": null, + "xml": null, + "formUrlEncoded": [], + "multipartForm": [] + }, + "headers": [], + "method": "GET", + "params": [ + { + "name": "userId", + "value": "", + "description": "", + "enabled": true, + "type": "path", + "uid": "mockeduuidvalue123456" + } + ], + "script": { + "res": null + }, + "url": "{{baseUrl}}/api/users/:userId/posts" + }, + "seq": 1 + } + ] + } + ] + } + ] + } + ] + } + ] +}; \ No newline at end of file diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js index a491f0c5ce..b2b05a18e7 100644 --- a/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno.spec.js @@ -48,6 +48,9 @@ servers: `; const expectedOutput = { + "name": "Hello World OpenAPI", + "uid": "mockeduuidvalue123456", + "version": "1", "environments": [ { "name": "Environment 1", @@ -97,7 +100,7 @@ const expectedOutput = { "uid": "mockeduuidvalue123456", }, ], - "name": "Folder1", + "name": "get", "type": "folder", "uid": "mockeduuidvalue123456", },