@gyeonghokim/bruno-to-openapi 0.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +5 -2
- package/.github/workflows/test.yml +37 -0
- package/.releaserc.json +3 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/models/bruno-collection.d.ts +35 -0
- package/dist/models/bruno-collection.d.ts.map +1 -0
- package/dist/models/bruno-collection.js +56 -0
- package/dist/services/conversion-service.d.ts +17 -0
- package/dist/services/conversion-service.d.ts.map +1 -0
- package/dist/services/conversion-service.js +18 -0
- package/dist/types/bruno.d.ts +267 -0
- package/dist/types/bruno.d.ts.map +1 -0
- package/dist/types/bruno.js +4 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/openapi.d.ts +251 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +4 -0
- package/dist/types/result.d.ts +26 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +4 -0
- package/dist/utils/bruno-parser.d.ts +51 -0
- package/dist/utils/bruno-parser.d.ts.map +1 -0
- package/dist/utils/bruno-parser.js +391 -0
- package/dist/utils/file-reader.d.ts +34 -0
- package/dist/utils/file-reader.d.ts.map +1 -0
- package/dist/utils/file-reader.js +104 -0
- package/dist/utils/openapi-generator.d.ts +53 -0
- package/dist/utils/openapi-generator.d.ts.map +1 -0
- package/dist/utils/openapi-generator.js +524 -0
- package/package.json +14 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { FileReader } from './file-reader';
|
|
3
|
+
/**
|
|
4
|
+
* Utility functions for parsing Bruno collection structures
|
|
5
|
+
*/
|
|
6
|
+
export class BrunoParser {
|
|
7
|
+
/**
|
|
8
|
+
* Parses a Bruno collection from a directory path
|
|
9
|
+
*/
|
|
10
|
+
static async parseCollection(collectionPath) {
|
|
11
|
+
// Check if the path exists and is a directory
|
|
12
|
+
const isDir = await FileReader.isDirectory(collectionPath);
|
|
13
|
+
if (!isDir) {
|
|
14
|
+
throw new Error(`Collection path does not exist or is not a directory: ${collectionPath}`);
|
|
15
|
+
}
|
|
16
|
+
// Look for the required collection files
|
|
17
|
+
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
|
18
|
+
const collectionBruPath = path.join(collectionPath, 'collection.bru');
|
|
19
|
+
let collection = {
|
|
20
|
+
version: '1',
|
|
21
|
+
uid: BrunoParser.generateUid(), // We'll generate a UID if not provided
|
|
22
|
+
name: path.basename(collectionPath), // Default to directory name
|
|
23
|
+
items: [],
|
|
24
|
+
pathname: collectionPath,
|
|
25
|
+
brunoConfig: undefined,
|
|
26
|
+
};
|
|
27
|
+
// Try to read bruno.json if it exists
|
|
28
|
+
if (await FileReader.fileExists(brunoJsonPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const brunoJsonContent = await FileReader.readFile(brunoJsonPath);
|
|
31
|
+
let brunoJson;
|
|
32
|
+
try {
|
|
33
|
+
brunoJson = JSON.parse(brunoJsonContent);
|
|
34
|
+
}
|
|
35
|
+
catch (parseError) {
|
|
36
|
+
throw new Error(`Invalid JSON in bruno.json: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`);
|
|
37
|
+
}
|
|
38
|
+
// Validate that brunoJson is an object
|
|
39
|
+
if (typeof brunoJson !== 'object' || brunoJson === null) {
|
|
40
|
+
throw new Error('bruno.json must contain a valid JSON object');
|
|
41
|
+
}
|
|
42
|
+
// Type guard to check if it has the required properties
|
|
43
|
+
if (typeof brunoJson.name === 'string' &&
|
|
44
|
+
brunoJson.name) {
|
|
45
|
+
collection = { ...collection, name: brunoJson.name };
|
|
46
|
+
}
|
|
47
|
+
// Validate version
|
|
48
|
+
if (typeof brunoJson.version === 'string' &&
|
|
49
|
+
['1'].includes(brunoJson.version)) {
|
|
50
|
+
collection = { ...collection, version: '1' };
|
|
51
|
+
}
|
|
52
|
+
// Store bruno config
|
|
53
|
+
if (typeof brunoJson === 'object' && brunoJson !== null) {
|
|
54
|
+
collection = { ...collection, brunoConfig: brunoJson };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof Error && error.message.includes('Invalid JSON')) {
|
|
59
|
+
throw error; // Re-throw specific JSON errors
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Failed to parse bruno.json: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Try to read collection.bru if it exists
|
|
65
|
+
if (await FileReader.fileExists(collectionBruPath)) {
|
|
66
|
+
try {
|
|
67
|
+
const collectionBruContent = await FileReader.readFile(collectionBruPath);
|
|
68
|
+
// TODO: Parse collection.bru content properly using bruno-lang if needed
|
|
69
|
+
// For now, we'll just acknowledge its existence
|
|
70
|
+
collection.root = { type: 'collection', name: collection.name };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new Error(`Failed to parse collection.bru: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Parse all .bru files in the collection directory
|
|
77
|
+
const bruFiles = await FileReader.getBruFiles(collectionPath);
|
|
78
|
+
if (bruFiles.length === 0) {
|
|
79
|
+
console.warn(`No .bru files found in collection directory: ${collectionPath}`);
|
|
80
|
+
}
|
|
81
|
+
const collectionItems = await BrunoParser.parseBruFiles(bruFiles, collectionPath);
|
|
82
|
+
collection = { ...collection, items: collectionItems };
|
|
83
|
+
return collection;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parses individual .bru files into BrunoItem structures
|
|
87
|
+
*/
|
|
88
|
+
static async parseBruFiles(bruFilePaths, collectionPath) {
|
|
89
|
+
const items = [];
|
|
90
|
+
for (const bruPath of bruFilePaths) {
|
|
91
|
+
try {
|
|
92
|
+
const content = await FileReader.readFile(bruPath);
|
|
93
|
+
const relativePath = path.relative(collectionPath, bruPath);
|
|
94
|
+
const parsedItem = await BrunoParser.parseBruContent(content, relativePath);
|
|
95
|
+
items.push(parsedItem);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw new Error(`Failed to parse .bru file ${bruPath}: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return items;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parses the content of a single .bru file into a BrunoItem
|
|
105
|
+
*/
|
|
106
|
+
static async parseBruContent(content, relativePath) {
|
|
107
|
+
// This is a more comprehensive parser for .bru files
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
const item = {
|
|
110
|
+
name: path.basename(relativePath, '.bru'),
|
|
111
|
+
pathname: relativePath,
|
|
112
|
+
type: 'http-request', // Default type
|
|
113
|
+
depth: relativePath.split('/').length - 1 || 0,
|
|
114
|
+
request: {}, // Initialize with empty request
|
|
115
|
+
};
|
|
116
|
+
// Parse the .bru content into sections
|
|
117
|
+
const sections = {};
|
|
118
|
+
let currentSection = null;
|
|
119
|
+
let currentSectionContent = [];
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmedLine = line.trim();
|
|
122
|
+
// Check if this is a section header like [sectionName]
|
|
123
|
+
if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
|
|
124
|
+
// Save previous section if exists
|
|
125
|
+
if (currentSection) {
|
|
126
|
+
sections[currentSection] = currentSectionContent;
|
|
127
|
+
}
|
|
128
|
+
// Start new section
|
|
129
|
+
currentSection = trimmedLine.substring(1, trimmedLine.length - 1);
|
|
130
|
+
currentSectionContent = [];
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Add line to current section
|
|
134
|
+
currentSectionContent.push(line);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Save the last section
|
|
138
|
+
if (currentSection) {
|
|
139
|
+
sections[currentSection] = currentSectionContent;
|
|
140
|
+
}
|
|
141
|
+
// Process each section
|
|
142
|
+
for (const [sectionName, sectionLines] of Object.entries(sections)) {
|
|
143
|
+
switch (sectionName) {
|
|
144
|
+
case 'meta':
|
|
145
|
+
BrunoParser.parseMetaSection(sectionLines, item);
|
|
146
|
+
break;
|
|
147
|
+
case 'request':
|
|
148
|
+
BrunoParser.parseRequestSection(sectionLines, item);
|
|
149
|
+
break;
|
|
150
|
+
case 'headers':
|
|
151
|
+
if (!item.request)
|
|
152
|
+
item.request = {};
|
|
153
|
+
item.request.headers = BrunoParser.parseHeadersSection(sectionLines);
|
|
154
|
+
break;
|
|
155
|
+
case 'params':
|
|
156
|
+
if (!item.request)
|
|
157
|
+
item.request = {};
|
|
158
|
+
item.request.params = BrunoParser.parseParamsSection(sectionLines);
|
|
159
|
+
break;
|
|
160
|
+
case 'body':
|
|
161
|
+
if (!item.request)
|
|
162
|
+
item.request = {};
|
|
163
|
+
item.request.body = BrunoParser.parseBodySection(sectionLines);
|
|
164
|
+
break;
|
|
165
|
+
case 'auth':
|
|
166
|
+
if (!item.request)
|
|
167
|
+
item.request = {};
|
|
168
|
+
item.request.auth = BrunoParser.parseAuthSection(sectionLines);
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
// Process general properties not in sections
|
|
172
|
+
for (const line of sectionLines) {
|
|
173
|
+
if (line.includes('=')) {
|
|
174
|
+
const parts = line.split('=', 2);
|
|
175
|
+
const key = parts[0];
|
|
176
|
+
const value = parts[1];
|
|
177
|
+
const trimmedKey = key?.trim();
|
|
178
|
+
const trimmedValue = value ? value.trim() : '';
|
|
179
|
+
if (trimmedKey === 'name') {
|
|
180
|
+
item.name = trimmedValue;
|
|
181
|
+
}
|
|
182
|
+
else if (trimmedKey === 'type' && trimmedValue) {
|
|
183
|
+
item.type = trimmedValue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// If no name was extracted, use the filename
|
|
190
|
+
if (!item.name) {
|
|
191
|
+
item.name = path.basename(relativePath, '.bru');
|
|
192
|
+
}
|
|
193
|
+
return item;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Parses the meta section of a .bru file
|
|
197
|
+
*/
|
|
198
|
+
static parseMetaSection(lines, item) {
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (line.includes('=')) {
|
|
201
|
+
const parts = line.split('=', 2);
|
|
202
|
+
const key = parts[0];
|
|
203
|
+
const value = parts[1];
|
|
204
|
+
const trimmedKey = key?.trim();
|
|
205
|
+
const trimmedValue = value ? value.trim() : '';
|
|
206
|
+
if (trimmedKey === 'name') {
|
|
207
|
+
item.name = trimmedValue;
|
|
208
|
+
}
|
|
209
|
+
else if (trimmedKey === 'seq') {
|
|
210
|
+
item.depth = Number.parseInt(trimmedValue, 10) || 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Parses the request section of a .bru file
|
|
217
|
+
*/
|
|
218
|
+
static parseRequestSection(lines, item) {
|
|
219
|
+
if (!item.request)
|
|
220
|
+
item.request = {};
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
if (line.includes('=')) {
|
|
223
|
+
const parts = line.split('=', 2);
|
|
224
|
+
const key = parts[0];
|
|
225
|
+
const value = parts[1];
|
|
226
|
+
const trimmedKey = key?.trim();
|
|
227
|
+
const trimmedValue = value ? value.trim() : '';
|
|
228
|
+
if (trimmedKey === 'method') {
|
|
229
|
+
item.request.method = trimmedValue;
|
|
230
|
+
}
|
|
231
|
+
else if (trimmedKey === 'url') {
|
|
232
|
+
item.request.url = trimmedValue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Parses the headers section of a .bru file
|
|
239
|
+
*/
|
|
240
|
+
static parseHeadersSection(lines) {
|
|
241
|
+
const headers = [];
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
if (line.includes('=')) {
|
|
244
|
+
const parts = line.split('=', 2);
|
|
245
|
+
const key = parts[0];
|
|
246
|
+
const value = parts[1];
|
|
247
|
+
const trimmedKey = key?.trim();
|
|
248
|
+
const trimmedValue = value ? value.trim() : '';
|
|
249
|
+
if (trimmedKey && trimmedKey !== 'enabled') {
|
|
250
|
+
// Skip the 'enabled' line if it's not part of a header
|
|
251
|
+
headers.push({
|
|
252
|
+
uid: BrunoParser.generateUid(),
|
|
253
|
+
name: trimmedKey,
|
|
254
|
+
value: trimmedValue,
|
|
255
|
+
enabled: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return headers;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parses the params section of a .bru file
|
|
264
|
+
*/
|
|
265
|
+
static parseParamsSection(lines) {
|
|
266
|
+
const params = [];
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
if (line.includes('=')) {
|
|
269
|
+
const parts = line.split('=', 2);
|
|
270
|
+
const key = parts[0];
|
|
271
|
+
const value = parts[1];
|
|
272
|
+
const trimmedKey = key?.trim();
|
|
273
|
+
const trimmedValue = value ? value.trim() : '';
|
|
274
|
+
if (trimmedKey && trimmedKey !== 'enabled') {
|
|
275
|
+
// Skip the 'enabled' line if it's not part of a param
|
|
276
|
+
params.push({
|
|
277
|
+
uid: BrunoParser.generateUid(),
|
|
278
|
+
name: trimmedKey,
|
|
279
|
+
value: trimmedValue,
|
|
280
|
+
type: 'query', // default type
|
|
281
|
+
enabled: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return params;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Parses the body section of a .bru file
|
|
290
|
+
*/
|
|
291
|
+
static parseBodySection(lines) {
|
|
292
|
+
const body = {};
|
|
293
|
+
for (const line of lines) {
|
|
294
|
+
if (line.includes('=')) {
|
|
295
|
+
const parts = line.split('=', 2);
|
|
296
|
+
const key = parts[0];
|
|
297
|
+
const value = parts[1];
|
|
298
|
+
const trimmedKey = key?.trim();
|
|
299
|
+
const trimmedValue = value ? value.trim() : '';
|
|
300
|
+
if (trimmedKey === 'mode') {
|
|
301
|
+
body.mode = trimmedValue;
|
|
302
|
+
}
|
|
303
|
+
else if (trimmedKey === 'json') {
|
|
304
|
+
body.json = trimmedValue;
|
|
305
|
+
}
|
|
306
|
+
else if (trimmedKey === 'xml') {
|
|
307
|
+
body.xml = trimmedValue;
|
|
308
|
+
}
|
|
309
|
+
else if (trimmedKey === 'text') {
|
|
310
|
+
body.text = trimmedValue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return body;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Parses the auth section of a .bru file
|
|
318
|
+
*/
|
|
319
|
+
static parseAuthSection(lines) {
|
|
320
|
+
const auth = { mode: 'none' };
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
if (line.includes('=')) {
|
|
323
|
+
const parts = line.split('=', 2);
|
|
324
|
+
const key = parts[0];
|
|
325
|
+
const value = parts[1];
|
|
326
|
+
const trimmedKey = key?.trim();
|
|
327
|
+
const trimmedValue = value ? value.trim() : '';
|
|
328
|
+
if (trimmedKey === 'mode') {
|
|
329
|
+
auth.mode = trimmedValue;
|
|
330
|
+
}
|
|
331
|
+
else if (trimmedKey === 'username') {
|
|
332
|
+
if (auth.mode === 'basic' || auth.mode === 'digest') {
|
|
333
|
+
if (!auth.basic)
|
|
334
|
+
auth.basic = { username: '', password: '' };
|
|
335
|
+
auth.basic.username = trimmedValue;
|
|
336
|
+
}
|
|
337
|
+
else if (auth.mode === 'oauth2') {
|
|
338
|
+
if (!auth.oauth2)
|
|
339
|
+
auth.oauth2 = { grantType: 'password', accessTokenUrl: '' };
|
|
340
|
+
// Note: OAuth2 has more complex structure that needs to be handled properly
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (trimmedKey === 'password') {
|
|
344
|
+
if (auth.mode === 'basic' || auth.mode === 'digest') {
|
|
345
|
+
if (!auth.basic)
|
|
346
|
+
auth.basic = { username: '', password: '' };
|
|
347
|
+
auth.basic.password = trimmedValue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (trimmedKey === 'token') {
|
|
351
|
+
if (auth.mode === 'bearer') {
|
|
352
|
+
if (!auth.bearer)
|
|
353
|
+
auth.bearer = { token: '' };
|
|
354
|
+
auth.bearer.token = trimmedValue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return auth;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Validates if a given path contains a valid Bruno collection
|
|
363
|
+
*/
|
|
364
|
+
static async isValidCollection(collectionPath) {
|
|
365
|
+
try {
|
|
366
|
+
// A Bruno collection should have either:
|
|
367
|
+
// 1. A bruno.json file, OR
|
|
368
|
+
// 2. At least one .bru file
|
|
369
|
+
const brunoJsonPath = path.join(collectionPath, 'bruno.json');
|
|
370
|
+
const hasBrunoJson = await FileReader.fileExists(brunoJsonPath);
|
|
371
|
+
if (hasBrunoJson) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
// Check for .bru files in the directory
|
|
375
|
+
const bruFiles = await FileReader.getBruFiles(collectionPath);
|
|
376
|
+
return bruFiles.length > 0;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Generates a unique identifier
|
|
384
|
+
*/
|
|
385
|
+
static generateUid() {
|
|
386
|
+
// In a real implementation, you might want to use nanoid or similar
|
|
387
|
+
// For now, we'll create a simple UID based on timestamp and random number
|
|
388
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=bruno-parser.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for reading files in the Bruno collection
|
|
3
|
+
*/
|
|
4
|
+
export declare class FileReader {
|
|
5
|
+
/**
|
|
6
|
+
* Reads a file and returns its content as a string
|
|
7
|
+
*/
|
|
8
|
+
static readFile(filePath: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a file exists
|
|
11
|
+
*/
|
|
12
|
+
static fileExists(filePath: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Checks if a path is a directory
|
|
15
|
+
*/
|
|
16
|
+
static isDirectory(dirPath: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Reads all files in a directory recursively
|
|
19
|
+
*/
|
|
20
|
+
static readDirRecursively(dirPath: string): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Gets all JSON files in a directory
|
|
23
|
+
*/
|
|
24
|
+
static getJsonFiles(dirPath: string): Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Gets all .bru files in a directory
|
|
27
|
+
*/
|
|
28
|
+
static getBruFiles(dirPath: string): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Safely resolves a path to prevent path traversal
|
|
31
|
+
*/
|
|
32
|
+
static safeResolve(basePath: string, relativePath: string): string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=file-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-reader.d.ts","sourceRoot":"","sources":["../../src/utils/file-reader.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,UAAU;IACrB;;OAEG;WACU,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBxD;;OAEG;WACU,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3D;;OAEG;WACU,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsBnE;;OAEG;WACU,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK7D;;OAEG;WACU,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAK5D;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;CAWnE"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Utility functions for reading files in the Bruno collection
|
|
5
|
+
*/
|
|
6
|
+
export class FileReader {
|
|
7
|
+
/**
|
|
8
|
+
* Reads a file and returns its content as a string
|
|
9
|
+
*/
|
|
10
|
+
static async readFile(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
// Validate file path to prevent path traversal attacks
|
|
13
|
+
const resolvedPath = path.resolve(filePath);
|
|
14
|
+
if (!(resolvedPath.startsWith(path.resolve('.')) || resolvedPath.startsWith(process.cwd()))) {
|
|
15
|
+
throw new Error('Invalid file path: path traversal detected');
|
|
16
|
+
}
|
|
17
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT') {
|
|
21
|
+
throw new Error(`File does not exist: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
if (error.code === 'EACCES') {
|
|
24
|
+
throw new Error(`Access denied reading file: ${filePath}`);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a file exists
|
|
31
|
+
*/
|
|
32
|
+
static async fileExists(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(filePath);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Checks if a path is a directory
|
|
43
|
+
*/
|
|
44
|
+
static async isDirectory(dirPath) {
|
|
45
|
+
try {
|
|
46
|
+
const stats = await fs.stat(dirPath);
|
|
47
|
+
return stats.isDirectory();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads all files in a directory recursively
|
|
55
|
+
*/
|
|
56
|
+
static async readDirRecursively(dirPath) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
// Prevent symbolic link loops and other path issues
|
|
62
|
+
const fullPath = path.resolve(dirPath, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
const nestedFiles = await FileReader.readDirRecursively(fullPath);
|
|
65
|
+
files.push(...nestedFiles);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
files.push(fullPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error(`Failed to read directory ${dirPath}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Gets all JSON files in a directory
|
|
79
|
+
*/
|
|
80
|
+
static async getJsonFiles(dirPath) {
|
|
81
|
+
const files = await FileReader.readDirRecursively(dirPath);
|
|
82
|
+
return files.filter(file => path.extname(file) === '.json');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets all .bru files in a directory
|
|
86
|
+
*/
|
|
87
|
+
static async getBruFiles(dirPath) {
|
|
88
|
+
const files = await FileReader.readDirRecursively(dirPath);
|
|
89
|
+
return files.filter(file => path.extname(file) === '.bru');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Safely resolves a path to prevent path traversal
|
|
93
|
+
*/
|
|
94
|
+
static safeResolve(basePath, relativePath) {
|
|
95
|
+
const resolved = path.resolve(basePath, relativePath);
|
|
96
|
+
const normalizedBase = path.resolve(basePath);
|
|
97
|
+
// Ensure the resolved path is within the base path
|
|
98
|
+
if (!resolved.startsWith(normalizedBase)) {
|
|
99
|
+
throw new Error('Path traversal detected');
|
|
100
|
+
}
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=file-reader.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BrunoCollection } from '../types/bruno';
|
|
2
|
+
import type { ResponsesObject, SchemaObject } from '../types/openapi';
|
|
3
|
+
import type { ConvertResult } from '../types/result';
|
|
4
|
+
/**
|
|
5
|
+
* Utility functions for generating OpenAPI specifications
|
|
6
|
+
*/
|
|
7
|
+
export declare class OpenApiGenerator {
|
|
8
|
+
/**
|
|
9
|
+
* Generates an OpenAPI specification from a Bruno collection
|
|
10
|
+
*/
|
|
11
|
+
static generateOpenApiSpec(collection: BrunoCollection): ConvertResult;
|
|
12
|
+
/**
|
|
13
|
+
* Processes collection items to build OpenAPI paths
|
|
14
|
+
*/
|
|
15
|
+
private static processCollectionItems;
|
|
16
|
+
/**
|
|
17
|
+
* Processes an HTTP request item to add to OpenAPI paths
|
|
18
|
+
*/
|
|
19
|
+
private static processHttpRequest;
|
|
20
|
+
/**
|
|
21
|
+
* Creates an OpenAPI operation from a Bruno request
|
|
22
|
+
*/
|
|
23
|
+
private static createOperationFromRequest;
|
|
24
|
+
/**
|
|
25
|
+
* Creates OpenAPI parameters from Bruno parameters
|
|
26
|
+
*/
|
|
27
|
+
private static createParametersFromBrunoParams;
|
|
28
|
+
/**
|
|
29
|
+
* Creates OpenAPI header parameters from Bruno headers
|
|
30
|
+
*/
|
|
31
|
+
private static createParametersFromBrunoHeaders;
|
|
32
|
+
/**
|
|
33
|
+
* Creates security requirements from Bruno auth configuration
|
|
34
|
+
*/
|
|
35
|
+
private static createSecurityRequirementsFromAuth;
|
|
36
|
+
/**
|
|
37
|
+
* Creates an OpenAPI request body from Bruno body
|
|
38
|
+
*/
|
|
39
|
+
private static createRequestBodyFromBrunoBody;
|
|
40
|
+
/**
|
|
41
|
+
* Infers a schema from JSON content
|
|
42
|
+
*/
|
|
43
|
+
private static inferSchemaFromJson;
|
|
44
|
+
/**
|
|
45
|
+
* Infers a schema from a JavaScript value
|
|
46
|
+
*/
|
|
47
|
+
static inferSchemaFromValue(value: unknown): SchemaObject;
|
|
48
|
+
/**
|
|
49
|
+
* Creates default responses for an operation
|
|
50
|
+
*/
|
|
51
|
+
static createDefaultResponses(): ResponsesObject;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=openapi-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-generator.d.ts","sourceRoot":"","sources":["../../src/utils/openapi-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,eAAe,EAKhB,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAUV,eAAe,EACf,YAAY,EACb,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AAEpE;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,eAAe,GAAG,aAAa;IAsDtE;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAiBrC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAmFjC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,0BAA0B;IAgEzC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,+BAA+B;IAsB9C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,gCAAgC;IAmB/C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,kCAAkC;IAwCjD;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,8BAA8B;IA6H7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAUlC;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY;IAiDzD;;OAEG;IACH,MAAM,CAAC,sBAAsB,IAAI,eAAe;CA8EjD"}
|