Skip to content

Commit afd49d1

Browse files
committed
add: oc support for cli
1 parent 8590bac commit afd49d1

File tree

8 files changed

+215
-95
lines changed

8 files changed

+215
-95
lines changed

packages/bruno-cli/src/commands/run.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const { rpad } = require('../utils/common');
1515
const { getOptions } = require('../utils/bru');
1616
const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');
1717
const constants = require('../constants');
18-
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection');
18+
const { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');
1919
const { hasExecutableTestInScript } = require('../utils/request');
2020
const command = 'run [paths...]';
2121
const desc = 'Run one or more requests/folders';
@@ -359,21 +359,21 @@ const handler = async function (argv) {
359359
}
360360

361361
if (envFile || env) {
362+
const envExt = FORMAT_CONFIG[collection.format].ext;
362363
const envFilePath = envFile
363364
? path.resolve(collectionPath, envFile)
364-
: path.join(collectionPath, 'environments', `${env}.bru`);
365+
: path.join(collectionPath, 'environments', `${env}${envExt}`);
365366

366367
const envFileExists = await exists(envFilePath);
367368
if (!envFileExists) {
368-
const errorPath = envFile || `environments/${env}.bru`;
369+
const errorPath = envFile || `environments/${env}${envExt}`;
369370
console.error(chalk.red(`Environment file not found: `) + chalk.dim(errorPath));
370371

371372
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
372373
}
373374

374-
const ext = path.extname(envFilePath).toLowerCase();
375-
if (ext === '.json') {
376-
// Parse Bruno schema JSON environment
375+
const fileExt = path.extname(envFilePath).toLowerCase();
376+
if (fileExt === '.json') {
377377
let envJsonContent;
378378
try {
379379
envJsonContent = fs.readFileSync(envFilePath, 'utf8');
@@ -387,8 +387,12 @@ const handler = async function (argv) {
387387
console.error(chalk.red(`Failed to parse Environment JSON: ${err.message}`));
388388
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
389389
}
390+
} else if (fileExt === '.yml' || fileExt === '.yaml') {
391+
const envContent = fs.readFileSync(envFilePath, 'utf8');
392+
const envJson = parseEnvironment(envContent, { format: 'yml' });
393+
envVars = getEnvVars(envJson);
394+
envVars.__name__ = envFile ? path.basename(envFilePath, fileExt) : env;
390395
} else {
391-
// Default to .bru parsing
392396
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
393397
const envJson = parseEnvironment(envBruContent);
394398
envVars = getEnvVars(envJson);
@@ -596,10 +600,11 @@ const handler = async function (argv) {
596600
const runtime = getJsSandboxRuntime(sandbox);
597601

598602
const runSingleRequestByPathname = async (relativeItemPathname) => {
603+
const ext = FORMAT_CONFIG[collection.format].ext;
599604
return new Promise(async (resolve, reject) => {
600605
let itemPathname = path.join(collectionPath, relativeItemPathname);
601-
if (itemPathname && !itemPathname?.endsWith('.bru')) {
602-
itemPathname = `${itemPathname}.bru`;
606+
if (itemPathname && !itemPathname?.endsWith(ext)) {
607+
itemPathname = `${itemPathname}${ext}`;
603608
}
604609
const requestItem = cloneDeep(findItemInCollection(collection, itemPathname));
605610
if (requestItem) {

packages/bruno-cli/src/utils/collection.js

Lines changed: 61 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -7,116 +7,90 @@ const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringi
77
const constants = require('../constants');
88
const chalk = require('chalk');
99

10-
const createCollectionJsonFromPathname = (collectionPath) => {
11-
const environmentsPath = path.join(collectionPath, `environments`);
10+
const FORMAT_CONFIG = {
11+
yml: { ext: '.yml', collectionFile: 'opencollection.yml', folderFile: 'folder.yml' },
12+
bru: { ext: '.bru', collectionFile: 'collection.bru', folderFile: 'folder.bru' }
13+
};
1214

13-
// get the collection bruno json config [<collection-path>/bruno.json]
14-
const brunoConfig = getCollectionBrunoJsonConfig(collectionPath);
15+
const getCollectionFormat = (collectionPath) => {
16+
if (fs.existsSync(path.join(collectionPath, 'opencollection.yml'))) return 'yml';
17+
if (fs.existsSync(path.join(collectionPath, 'bruno.json'))) return 'bru';
18+
return null;
19+
};
1520

16-
// get the collection root [<collection-path>/collection.bru]
17-
const collectionRoot = getCollectionRoot(collectionPath);
21+
const getCollectionConfig = (collectionPath, format) => {
22+
if (format === 'yml') {
23+
const content = fs.readFileSync(path.join(collectionPath, 'opencollection.yml'), 'utf8');
24+
const parsed = parseCollection(content, { format: 'yml' });
25+
return { brunoConfig: parsed.brunoConfig, collectionRoot: parsed.collectionRoot || {} };
26+
}
27+
const brunoConfig = JSON.parse(fs.readFileSync(path.join(collectionPath, 'bruno.json'), 'utf8'));
28+
const collectionBruPath = path.join(collectionPath, 'collection.bru');
29+
const collectionRoot = fs.existsSync(collectionBruPath)
30+
? parseCollection(fs.readFileSync(collectionBruPath, 'utf8'), { format: 'bru' })
31+
: {};
32+
return { brunoConfig, collectionRoot };
33+
};
34+
35+
const getFolderRoot = (dir, format) => {
36+
const folderPath = path.join(dir, FORMAT_CONFIG[format].folderFile);
37+
if (!fs.existsSync(folderPath)) return null;
38+
return parseFolder(fs.readFileSync(folderPath, 'utf8'), { format });
39+
};
40+
41+
const createCollectionJsonFromPathname = (collectionPath) => {
42+
const format = getCollectionFormat(collectionPath);
43+
if (!format) {
44+
console.error(chalk.red(`You can run only at the root of a collection`));
45+
process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
46+
}
47+
48+
const { brunoConfig, collectionRoot } = getCollectionConfig(collectionPath, format);
49+
const { ext, collectionFile, folderFile } = FORMAT_CONFIG[format];
50+
const environmentsPath = path.join(collectionPath, 'environments');
1851

19-
// get the collection items recursively
2052
const traverse = (currentPath) => {
21-
const filesInCurrentDir = fs.readdirSync(currentPath);
22-
if (currentPath.includes('node_modules')) {
23-
return;
24-
}
53+
if (currentPath.includes('node_modules')) return [];
2554
const currentDirItems = [];
26-
for (const file of filesInCurrentDir) {
55+
56+
for (const file of fs.readdirSync(currentPath)) {
2757
const filePath = path.join(currentPath, file);
2858
const stats = fs.lstatSync(filePath);
59+
2960
if (stats.isDirectory()) {
30-
if (filePath === environmentsPath) continue;
31-
if (filePath.startsWith('.git') || filePath.startsWith('node_modules')) continue;
32-
33-
// get the folder root
34-
let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) };
35-
const folderBruJson = getFolderRoot(filePath);
36-
if (folderBruJson) {
37-
folderItem.root = folderBruJson;
38-
folderItem.seq = folderBruJson.meta.seq;
61+
if (filePath === environmentsPath || file === '.git' || file === 'node_modules') continue;
62+
const folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) };
63+
const folderRoot = getFolderRoot(filePath, format);
64+
if (folderRoot) {
65+
folderItem.root = folderRoot;
66+
folderItem.seq = folderRoot.meta?.seq;
3967
}
4068
currentDirItems.push(folderItem);
4169
} else {
42-
if (['collection.bru', 'folder.bru'].includes(file)) continue;
43-
if (path.extname(filePath) !== '.bru') continue;
44-
45-
// get the request item
70+
if (file === collectionFile || file === folderFile || path.extname(filePath) !== ext) continue;
4671
try {
47-
const bruContent = fs.readFileSync(filePath, 'utf8');
48-
const requestItem = parseRequest(bruContent);
49-
currentDirItems.push({
50-
name: file,
51-
pathname: filePath,
52-
...requestItem
53-
});
72+
const requestItem = parseRequest(fs.readFileSync(filePath, 'utf8'), { format });
73+
currentDirItems.push({ name: file, ...requestItem, pathname: filePath });
5474
} catch (err) {
55-
// Log warning for invalid .bru file but continue processing
5675
console.warn(chalk.yellow(`Warning: Skipping invalid file ${filePath}\nError: ${err.message}`));
57-
// Track skipped files for later reporting
58-
if (!global.brunoSkippedFiles) {
59-
global.brunoSkippedFiles = [];
60-
}
76+
global.brunoSkippedFiles = global.brunoSkippedFiles || [];
6177
global.brunoSkippedFiles.push({ path: filePath, error: err.message });
6278
}
6379
}
6480
}
65-
let currentDirFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder');
66-
let sortedFolderItems = sortByNameThenSequence(currentDirFolderItems);
6781

68-
let currentDirRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder');
69-
let sortedRequestItems = currentDirRequestItems?.sort((a, b) => a.seq - b.seq);
70-
71-
return sortedFolderItems?.concat(sortedRequestItems);
82+
const folders = sortByNameThenSequence(currentDirItems.filter((i) => i.type === 'folder'));
83+
const requests = currentDirItems.filter((i) => i.type !== 'folder').sort((a, b) => a.seq - b.seq);
84+
return folders.concat(requests);
7285
};
73-
let collectionItems = traverse(collectionPath);
7486

75-
let collection = {
87+
return {
7688
brunoConfig,
89+
format,
7790
root: collectionRoot,
7891
pathname: collectionPath,
79-
items: collectionItems
92+
items: traverse(collectionPath)
8093
};
81-
82-
return collection;
83-
};
84-
85-
const getCollectionBrunoJsonConfig = (dir) => {
86-
// right now, bru must be run from the root of the collection
87-
// will add support in the future to run it from anywhere inside the collection
88-
const brunoJsonPath = path.join(dir, 'bruno.json');
89-
const brunoJsonExists = fs.existsSync(brunoJsonPath);
90-
if (!brunoJsonExists) {
91-
console.error(chalk.red(`You can run only at the root of a collection`));
92-
process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);
93-
}
94-
95-
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
96-
const brunoConfig = JSON.parse(brunoConfigFile);
97-
return brunoConfig;
98-
};
99-
100-
const getCollectionRoot = (dir) => {
101-
const collectionRootPath = path.join(dir, 'collection.bru');
102-
const exists = fs.existsSync(collectionRootPath);
103-
if (!exists) {
104-
return {};
105-
}
106-
107-
const content = fs.readFileSync(collectionRootPath, 'utf8');
108-
return parseCollection(content);
109-
};
110-
111-
const getFolderRoot = (dir) => {
112-
const folderRootPath = path.join(dir, 'folder.bru');
113-
const exists = fs.existsSync(folderRootPath);
114-
if (!exists) {
115-
return null;
116-
}
117-
118-
const content = fs.readFileSync(folderRootPath, 'utf8');
119-
return parseFolder(content);
12094
};
12195

12296
const mergeHeaders = (collection, request, requestTreePath) => {
@@ -612,6 +586,8 @@ const sortByNameThenSequence = (items) => {
612586
};
613587

614588
module.exports = {
589+
FORMAT_CONFIG,
590+
getCollectionFormat,
615591
createCollectionJsonFromPathname,
616592
mergeHeaders,
617593
mergeVars,

packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const path = require('node:path');
2+
const fs = require('node:fs');
23
const { describe, it, expect } = require('@jest/globals');
34
const constants = require('../../src/constants');
4-
const { createCollectionJsonFromPathname } = require('../../src/utils/collection');
5+
const { createCollectionJsonFromPathname, getCollectionFormat, FORMAT_CONFIG } = require('../../src/utils/collection');
6+
const { parseEnvironment } = require('@usebruno/filestore');
57

68
describe('create collection json from pathname', () => {
79
it('should throw an error when the pathname is not a valid bruno collection root', () => {
@@ -169,4 +171,96 @@ describe('create collection json from pathname', () => {
169171
// tests
170172
expect(c).toHaveProperty('items[4].request.tests', 'test(\"request level script\", function() {\n expect(\"test\").to.equal(\"test\");\n});');
171173
});
174+
175+
it('creates a collection json from OpenCollection yml files', () => {
176+
const collectionPathname = path.join(__dirname, './fixtures/opencollection/collection');
177+
const c = createCollectionJsonFromPathname(collectionPathname);
178+
179+
expect(c).toBeDefined();
180+
expect(c).toHaveProperty('format', 'yml');
181+
expect(c).toHaveProperty('brunoConfig.opencollection', '1.0.0');
182+
expect(c).toHaveProperty('brunoConfig.name', 'Test OpenCollection');
183+
expect(c).toHaveProperty('brunoConfig.type', 'collection');
184+
expect(c).toHaveProperty('brunoConfig.ignore', ['node_modules', '.git']);
185+
expect(c).toHaveProperty('pathname', collectionPathname);
186+
187+
// collection root headers
188+
expect(c).toHaveProperty('root.request.headers[0].name', 'X-Collection-Header');
189+
expect(c).toHaveProperty('root.request.headers[0].value', 'collection-header-value');
190+
expect(c).toHaveProperty('root.request.headers[0].enabled', true);
191+
192+
// folder
193+
expect(c.items.some((i) => i.type === 'folder' && i.name === 'users')).toBe(true);
194+
const usersFolder = c.items.find((i) => i.name === 'users');
195+
expect(usersFolder).toHaveProperty('root.meta.name', 'Users');
196+
expect(usersFolder).toHaveProperty('root.meta.seq', 1);
197+
expect(usersFolder.pathname).toContain('users');
198+
199+
// request in folder - name comes from info.name, pathname is correct
200+
const createUserReq = usersFolder.items.find((i) => i.name === 'Create User');
201+
expect(createUserReq).toBeDefined();
202+
expect(createUserReq).toHaveProperty('type', 'http-request');
203+
expect(createUserReq).toHaveProperty('request.method', 'POST');
204+
expect(createUserReq).toHaveProperty('request.url', 'https://api.example.com/users');
205+
expect(createUserReq.pathname).toContain('create-user.yml');
206+
207+
// root level request - name comes from info.name, pathname is correct
208+
const getUsersReq = c.items.find((i) => i.name === 'Get Users');
209+
expect(getUsersReq).toBeDefined();
210+
expect(getUsersReq).toHaveProperty('type', 'http-request');
211+
expect(getUsersReq).toHaveProperty('request.method', 'GET');
212+
expect(getUsersReq).toHaveProperty('request.url', 'https://api.example.com/users');
213+
expect(getUsersReq.pathname).toContain('get-users.yml');
214+
});
215+
});
216+
217+
describe('getCollectionFormat', () => {
218+
it('returns yml for OpenCollection', () => {
219+
const collectionPath = path.join(__dirname, './fixtures/opencollection/collection');
220+
expect(getCollectionFormat(collectionPath)).toBe('yml');
221+
});
222+
223+
it('returns bru for Bruno collection', () => {
224+
const collectionPath = path.join(__dirname, './fixtures/collection-json-from-pathname/collection');
225+
expect(getCollectionFormat(collectionPath)).toBe('bru');
226+
});
227+
228+
it('returns null for invalid path', () => {
229+
const collectionPath = path.join(__dirname, './fixtures/collection-invalid');
230+
expect(getCollectionFormat(collectionPath)).toBe(null);
231+
});
232+
});
233+
234+
describe('FORMAT_CONFIG', () => {
235+
it('has correct config for yml format', () => {
236+
expect(FORMAT_CONFIG.yml).toEqual({
237+
ext: '.yml',
238+
collectionFile: 'opencollection.yml',
239+
folderFile: 'folder.yml'
240+
});
241+
});
242+
243+
it('has correct config for bru format', () => {
244+
expect(FORMAT_CONFIG.bru).toEqual({
245+
ext: '.bru',
246+
collectionFile: 'collection.bru',
247+
folderFile: 'folder.bru'
248+
});
249+
});
250+
});
251+
252+
describe('OpenCollection environment parsing', () => {
253+
it('parses YML environment files correctly', () => {
254+
const envPath = path.join(__dirname, './fixtures/opencollection/collection/environments/dev.yml');
255+
const envContent = fs.readFileSync(envPath, 'utf8');
256+
const env = parseEnvironment(envContent, { format: 'yml' });
257+
258+
expect(env).toBeDefined();
259+
expect(env).toHaveProperty('name', 'Development');
260+
expect(env.variables).toHaveLength(2);
261+
expect(env.variables[0]).toHaveProperty('name', 'baseUrl');
262+
expect(env.variables[0]).toHaveProperty('value', 'https://api.dev.example.com');
263+
expect(env.variables[1]).toHaveProperty('name', 'apiKey');
264+
expect(env.variables[1]).toHaveProperty('value', 'dev-api-key-123');
265+
});
172266
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: Development
2+
variables:
3+
- name: baseUrl
4+
value: https://api.dev.example.com
5+
- name: apiKey
6+
value: dev-api-key-123
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
info:
2+
name: Get Users
3+
type: http
4+
seq: 1
5+
6+
http:
7+
method: GET
8+
url: https://api.example.com/users
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
opencollection: "1.0.0"
2+
info:
3+
name: Test OpenCollection
4+
5+
extensions:
6+
ignore:
7+
- node_modules
8+
- .git
9+
10+
request:
11+
headers:
12+
- name: X-Collection-Header
13+
value: collection-header-value
14+
enabled: true

0 commit comments

Comments
 (0)