@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.
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Utility functions for generating OpenAPI specifications
3
+ */
4
+ export class OpenApiGenerator {
5
+ /**
6
+ * Generates an OpenAPI specification from a Bruno collection
7
+ */
8
+ static generateOpenApiSpec(collection) {
9
+ const warnings = [];
10
+ const info = {
11
+ title: collection.name || 'Bruno Collection',
12
+ version: collection.brunoConfig &&
13
+ typeof collection.brunoConfig === 'object' &&
14
+ 'version' in collection.brunoConfig
15
+ ? String(collection.brunoConfig.version) || '1.0.0'
16
+ : '1.0.0',
17
+ description: collection.brunoConfig &&
18
+ typeof collection.brunoConfig === 'object' &&
19
+ 'description' in collection.brunoConfig
20
+ ? String(collection.brunoConfig.description) ||
21
+ `OpenAPI specification generated from Bruno collection: ${collection.name}`
22
+ : `OpenAPI specification generated from Bruno collection: ${collection.name}`,
23
+ };
24
+ const paths = {};
25
+ // Process collection items to build paths
26
+ OpenApiGenerator.processCollectionItems(collection.items, paths, warnings);
27
+ const openApiSpec = {
28
+ openapi: '3.0.0',
29
+ info,
30
+ paths,
31
+ };
32
+ // Add servers if base URL is defined in collection
33
+ if (collection.brunoConfig &&
34
+ typeof collection.brunoConfig === 'object' &&
35
+ 'baseUrl' in collection.brunoConfig) {
36
+ const baseUrl = collection.brunoConfig.baseUrl;
37
+ if (typeof baseUrl === 'string' && baseUrl) {
38
+ openApiSpec.servers = [
39
+ {
40
+ url: baseUrl,
41
+ },
42
+ ];
43
+ }
44
+ }
45
+ return {
46
+ spec: openApiSpec,
47
+ warnings,
48
+ content: JSON.stringify(openApiSpec, null, 2), // Optional JSON content
49
+ };
50
+ }
51
+ /**
52
+ * Processes collection items to build OpenAPI paths
53
+ */
54
+ static processCollectionItems(items, paths, warnings) {
55
+ for (const item of items) {
56
+ if (item.type === 'folder') {
57
+ // Process folder items recursively
58
+ if (item.items && item.items.length > 0) {
59
+ OpenApiGenerator.processCollectionItems(item.items, paths, warnings);
60
+ }
61
+ }
62
+ else if (item.type === 'http-request' && item.request) {
63
+ OpenApiGenerator.processHttpRequest(item, paths, warnings);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Processes an HTTP request item to add to OpenAPI paths
69
+ */
70
+ static processHttpRequest(item, paths, warnings) {
71
+ const request = item.request;
72
+ if (!(request?.method && request.url)) {
73
+ warnings.push({
74
+ message: `Skipping item '${item.name}' - missing method or URL`,
75
+ itemName: item.name,
76
+ });
77
+ return;
78
+ }
79
+ // Normalize the URL to extract path
80
+ let normalizedPath = request.url || '';
81
+ // Remove the base URL part if it exists
82
+ if (request.url?.includes('://')) {
83
+ try {
84
+ const urlObj = new URL(request.url);
85
+ normalizedPath = urlObj.pathname + urlObj.search;
86
+ }
87
+ catch (e) {
88
+ // If URL parsing fails, use the original URL
89
+ warnings.push({
90
+ message: `Could not parse URL for item '${item.name}': ${request.url}`,
91
+ itemName: item.name,
92
+ });
93
+ // Fallback to using the original URL as the path
94
+ normalizedPath = request.url;
95
+ }
96
+ }
97
+ // Ensure path starts with /
98
+ if (!normalizedPath.startsWith('/')) {
99
+ normalizedPath = `/${normalizedPath}`;
100
+ }
101
+ // Initialize the path object if it doesn't exist
102
+ if (!paths[normalizedPath]) {
103
+ paths[normalizedPath] = {};
104
+ }
105
+ // Create operation based on the request
106
+ const operation = OpenApiGenerator.createOperationFromRequest(item, request);
107
+ // Add the operation to the appropriate method in the path
108
+ const pathObject = paths[normalizedPath];
109
+ if (pathObject) {
110
+ switch (request.method?.toUpperCase()) {
111
+ case 'GET':
112
+ pathObject.get = operation;
113
+ break;
114
+ case 'POST':
115
+ pathObject.post = operation;
116
+ break;
117
+ case 'PUT':
118
+ pathObject.put = operation;
119
+ break;
120
+ case 'DELETE':
121
+ pathObject.delete = operation;
122
+ break;
123
+ case 'PATCH':
124
+ pathObject.patch = operation;
125
+ break;
126
+ case 'HEAD':
127
+ pathObject.head = operation;
128
+ break;
129
+ case 'OPTIONS':
130
+ pathObject.options = operation;
131
+ break;
132
+ case 'TRACE':
133
+ pathObject.trace = operation;
134
+ break;
135
+ default:
136
+ warnings.push({
137
+ message: `Unsupported HTTP method '${request.method}' for item '${item.name}'`,
138
+ itemName: item.name,
139
+ });
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Creates an OpenAPI operation from a Bruno request
145
+ */
146
+ static createOperationFromRequest(item, request) {
147
+ const operation = {
148
+ summary: item.name,
149
+ description: request?.docs || `Operation for ${item.name}`,
150
+ responses: OpenApiGenerator.createDefaultResponses(),
151
+ };
152
+ // Add tags if available in Bruno config
153
+ if (request?.tags && Array.isArray(request.tags)) {
154
+ operation.tags = request.tags;
155
+ }
156
+ else {
157
+ // Default to using the folder path as tags
158
+ if (item.pathname) {
159
+ const folderPath = item.pathname.substring(0, item.pathname.lastIndexOf('/'));
160
+ if (folderPath) {
161
+ operation.tags = [folderPath.split('/').pop() || 'default'];
162
+ }
163
+ }
164
+ }
165
+ // Add parameters from Bruno request
166
+ if (request?.params && request.params.length > 0) {
167
+ const pathParams = request.params.filter(param => param.enabled !== false && param.type === 'path');
168
+ const queryParams = request.params.filter(param => param.enabled !== false && param.type === 'query');
169
+ if (pathParams.length > 0) {
170
+ const pathParamObjs = OpenApiGenerator.createParametersFromBrunoParams(pathParams);
171
+ operation.parameters = [...(operation.parameters || []), ...pathParamObjs];
172
+ }
173
+ if (queryParams.length > 0) {
174
+ const queryParamObjs = OpenApiGenerator.createParametersFromBrunoParams(queryParams);
175
+ operation.parameters = [...(operation.parameters || []), ...queryParamObjs];
176
+ }
177
+ }
178
+ // Add headers as parameters if needed
179
+ if (request?.headers && request.headers.length > 0) {
180
+ const headerParams = OpenApiGenerator.createParametersFromBrunoHeaders(request.headers);
181
+ if (headerParams.length > 0) {
182
+ operation.parameters = [...(operation.parameters || []), ...headerParams];
183
+ }
184
+ }
185
+ // Add request body if present
186
+ if (request?.body) {
187
+ operation.requestBody = OpenApiGenerator.createRequestBodyFromBrunoBody(request.body);
188
+ }
189
+ // Add authentication information to operation if needed
190
+ if (request?.auth) {
191
+ operation.security = OpenApiGenerator.createSecurityRequirementsFromAuth(request.auth);
192
+ }
193
+ return operation;
194
+ }
195
+ /**
196
+ * Creates OpenAPI parameters from Bruno parameters
197
+ */
198
+ static createParametersFromBrunoParams(brunoParams) {
199
+ return brunoParams
200
+ .filter(param => param.enabled !== false) // Only enabled params
201
+ .map(param => {
202
+ // Determine if parameter is required based on Bruno param content
203
+ const isRequired = param.value !== undefined && param.value !== '';
204
+ const paramObj = {
205
+ name: param.name || '',
206
+ in: param.type || 'query', // Use the Bruno param's type or default to query
207
+ required: isRequired,
208
+ schema: {
209
+ type: 'string',
210
+ example: param.value || undefined,
211
+ },
212
+ description: param.name || '',
213
+ };
214
+ return paramObj;
215
+ });
216
+ }
217
+ /**
218
+ * Creates OpenAPI header parameters from Bruno headers
219
+ */
220
+ static createParametersFromBrunoHeaders(brunoHeaders) {
221
+ return brunoHeaders
222
+ .filter(header => header.enabled !== false) // Only enabled headers
223
+ .map(header => {
224
+ const isRequired = header.value !== undefined && header.value !== '';
225
+ return {
226
+ name: header.name || '',
227
+ in: 'header',
228
+ required: isRequired,
229
+ schema: {
230
+ type: 'string',
231
+ example: header.value || undefined,
232
+ },
233
+ description: header.name || '',
234
+ };
235
+ });
236
+ }
237
+ /**
238
+ * Creates security requirements from Bruno auth configuration
239
+ */
240
+ static createSecurityRequirementsFromAuth(brunoAuth) {
241
+ if (brunoAuth.mode === 'none') {
242
+ return undefined;
243
+ }
244
+ const securityRequirements = [];
245
+ switch (brunoAuth.mode) {
246
+ case 'bearer':
247
+ securityRequirements.push({
248
+ bearerAuth: [],
249
+ });
250
+ break;
251
+ case 'basic':
252
+ securityRequirements.push({
253
+ basicAuth: [],
254
+ });
255
+ break;
256
+ case 'oauth2':
257
+ securityRequirements.push({
258
+ oauth2: [],
259
+ });
260
+ break;
261
+ case 'apikey':
262
+ securityRequirements.push({
263
+ apiKey: [],
264
+ });
265
+ break;
266
+ default:
267
+ // For other auth types, we create a generic security requirement
268
+ securityRequirements.push({
269
+ [`${brunoAuth.mode}Auth`]: [],
270
+ });
271
+ }
272
+ return securityRequirements;
273
+ }
274
+ /**
275
+ * Creates an OpenAPI request body from Bruno body
276
+ */
277
+ static createRequestBodyFromBrunoBody(brunoBody) {
278
+ if (!brunoBody.mode) {
279
+ return undefined;
280
+ }
281
+ const requestBody = {
282
+ content: {},
283
+ };
284
+ switch (brunoBody.mode) {
285
+ case 'json':
286
+ if (brunoBody.json) {
287
+ try {
288
+ // Attempt to parse JSON to create a proper schema
289
+ const parsedJson = JSON.parse(brunoBody.json);
290
+ const schema = OpenApiGenerator.inferSchemaFromValue(parsedJson);
291
+ requestBody.content['application/json'] = {
292
+ schema,
293
+ example: parsedJson,
294
+ };
295
+ }
296
+ catch (e) {
297
+ // If JSON parsing fails, use string schema
298
+ requestBody.content['application/json'] = {
299
+ schema: { type: 'string' },
300
+ example: brunoBody.json,
301
+ };
302
+ }
303
+ }
304
+ else {
305
+ // If no JSON content provided, use an empty object schema as default
306
+ requestBody.content['application/json'] = {
307
+ schema: { type: 'object', properties: {} },
308
+ };
309
+ }
310
+ break;
311
+ case 'xml':
312
+ if (brunoBody.xml) {
313
+ requestBody.content['application/xml'] = {
314
+ schema: { type: 'string' },
315
+ example: brunoBody.xml,
316
+ };
317
+ }
318
+ break;
319
+ case 'formUrlEncoded':
320
+ if (brunoBody.formUrlEncoded && brunoBody.formUrlEncoded.length > 0) {
321
+ const schema = {
322
+ type: 'object',
323
+ properties: {},
324
+ };
325
+ for (const param of brunoBody.formUrlEncoded) {
326
+ const paramName = param.name || 'defaultParam';
327
+ schema.properties[paramName] =
328
+ {
329
+ type: 'string',
330
+ example: param.value || undefined,
331
+ };
332
+ }
333
+ requestBody.content['application/x-www-form-urlencoded'] = {
334
+ schema,
335
+ };
336
+ }
337
+ else {
338
+ // Default to empty object if no form parameters
339
+ requestBody.content['application/x-www-form-urlencoded'] = {
340
+ schema: {
341
+ type: 'object',
342
+ properties: {},
343
+ },
344
+ };
345
+ }
346
+ break;
347
+ case 'multipartForm':
348
+ if (brunoBody.multipartForm && brunoBody.multipartForm.length > 0) {
349
+ const schema = {
350
+ type: 'object',
351
+ properties: {},
352
+ };
353
+ for (const param of brunoBody.multipartForm) {
354
+ const paramName = param.name || 'defaultParam';
355
+ schema.properties[paramName] =
356
+ {
357
+ type: param.type === 'file' ? 'string' : 'string',
358
+ format: param.type === 'file' ? 'binary' : undefined,
359
+ example: param.value || undefined,
360
+ };
361
+ }
362
+ requestBody.content['multipart/form-data'] = {
363
+ schema,
364
+ };
365
+ }
366
+ else {
367
+ // Default to empty object if no multipart form parameters
368
+ requestBody.content['multipart/form-data'] = {
369
+ schema: {
370
+ type: 'object',
371
+ properties: {},
372
+ },
373
+ };
374
+ }
375
+ break;
376
+ case 'text':
377
+ if (brunoBody.text) {
378
+ requestBody.content['text/plain'] = {
379
+ schema: { type: 'string' },
380
+ example: brunoBody.text,
381
+ };
382
+ }
383
+ else {
384
+ requestBody.content['text/plain'] = {
385
+ schema: { type: 'string' },
386
+ };
387
+ }
388
+ break;
389
+ default:
390
+ // For unknown modes, add a generic application/json content type
391
+ requestBody.content['application/json'] = {
392
+ schema: { type: 'object' },
393
+ };
394
+ }
395
+ return requestBody;
396
+ }
397
+ /**
398
+ * Infers a schema from JSON content
399
+ */
400
+ static inferSchemaFromJson(jsonStr) {
401
+ try {
402
+ const parsed = JSON.parse(jsonStr);
403
+ return OpenApiGenerator.inferSchemaFromValue(parsed);
404
+ }
405
+ catch (e) {
406
+ // If JSON parsing fails, return a generic string schema
407
+ return { type: 'string', description: 'Could not parse JSON content, treating as string' };
408
+ }
409
+ }
410
+ /**
411
+ * Infers a schema from a JavaScript value
412
+ */
413
+ static inferSchemaFromValue(value) {
414
+ const schema = {
415
+ type: typeof value,
416
+ };
417
+ if (value === null) {
418
+ schema.type = 'null';
419
+ return schema;
420
+ }
421
+ if (Array.isArray(value)) {
422
+ schema.type = 'array';
423
+ if (value.length > 0) {
424
+ // Use the first element to infer item schema
425
+ schema.items = OpenApiGenerator.inferSchemaFromValue(value[0]);
426
+ }
427
+ else {
428
+ // For empty arrays, provide a generic item schema
429
+ schema.items = { type: 'object' };
430
+ }
431
+ return schema;
432
+ }
433
+ if (typeof value === 'object' && value !== null) {
434
+ schema.type = 'object';
435
+ schema.properties = {};
436
+ // Use for...of to avoid forEach and non-null assertion
437
+ for (const [key, propertyValue] of Object.entries(value)) {
438
+ const propertySchema = OpenApiGenerator.inferSchemaFromValue(propertyValue);
439
+ if (schema.properties) {
440
+ schema.properties[key] = propertySchema;
441
+ }
442
+ }
443
+ return schema;
444
+ }
445
+ // For primitive types, just return the basic schema
446
+ return schema;
447
+ }
448
+ /**
449
+ * Creates default responses for an operation
450
+ */
451
+ static createDefaultResponses() {
452
+ const responses = {};
453
+ // Add a default 200 OK response
454
+ responses['200'] = {
455
+ description: 'Successful response',
456
+ content: {
457
+ 'application/json': {
458
+ schema: {
459
+ type: 'object',
460
+ description: 'Default response schema',
461
+ },
462
+ },
463
+ },
464
+ };
465
+ // Add 400 Bad Request response
466
+ responses['400'] = {
467
+ description: 'Bad Request - The request was invalid',
468
+ content: {
469
+ 'application/json': {
470
+ schema: {
471
+ type: 'object',
472
+ properties: {
473
+ error: { type: 'string', description: 'Error message' },
474
+ },
475
+ },
476
+ },
477
+ },
478
+ };
479
+ // Add 401 Unauthorized response
480
+ responses['401'] = {
481
+ description: 'Unauthorized - Authentication required',
482
+ content: {
483
+ 'application/json': {
484
+ schema: {
485
+ type: 'object',
486
+ properties: {
487
+ error: { type: 'string', description: 'Error message' },
488
+ },
489
+ },
490
+ },
491
+ },
492
+ };
493
+ // Add 404 Not Found response
494
+ responses['404'] = {
495
+ description: 'Not Found - The requested resource does not exist',
496
+ content: {
497
+ 'application/json': {
498
+ schema: {
499
+ type: 'object',
500
+ properties: {
501
+ error: { type: 'string', description: 'Error message' },
502
+ },
503
+ },
504
+ },
505
+ },
506
+ };
507
+ // Add 500 Internal Server Error response
508
+ responses['500'] = {
509
+ description: 'Internal Server Error',
510
+ content: {
511
+ 'application/json': {
512
+ schema: {
513
+ type: 'object',
514
+ properties: {
515
+ error: { type: 'string', description: 'Error message' },
516
+ },
517
+ },
518
+ },
519
+ },
520
+ };
521
+ return responses;
522
+ }
523
+ }
524
+ //# sourceMappingURL=openapi-generator.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gyeonghokim/bruno-to-openapi",
3
3
  "private": false,
4
- "version": "0.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "Convert Bruno API collections to OpenAPI 3.0 specification",
6
6
  "type": "module",
7
7
  "module": "./dist/index.js",
@@ -12,6 +12,15 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/",
17
+ "tag": "latest",
18
+ "provenance": true
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/GyeongHoKim/bruno2openapi.git"
23
+ },
15
24
  "scripts": {
16
25
  "build": "tsc",
17
26
  "test": "vitest run",
@@ -36,7 +45,11 @@
36
45
  "devDependencies": {
37
46
  "@apidevtools/swagger-parser": "^12.1.0",
38
47
  "@biomejs/biome": "^1.0.0",
48
+ "@semantic-release/commit-analyzer": "^13.0.1",
49
+ "@semantic-release/npm": "^13.1.3",
50
+ "@semantic-release/release-notes-generator": "^14.1.0",
39
51
  "@types/node": "^18.0.0",
52
+ "semantic-release": "^25.0.2",
40
53
  "typescript": "^5.9.3",
41
54
  "vitest": "^1.0.0"
42
55
  },