Skip to content

Commit f8653cc

Browse files
committed
refactor
1 parent e5fba95 commit f8653cc

File tree

1 file changed

+143
-42
lines changed

1 file changed

+143
-42
lines changed

packages/bruno-converters/src/openapi/openapi-to-bruno.js

Lines changed: 143 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import get from 'lodash/get';
33
import jsyaml from 'js-yaml';
44
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
55

6+
// Content type patterns for matching MIME type variants
7+
// These patterns handle structured types with many variants (e.g., application/ld+json, application/vnd.api+json)
8+
const CONTENT_TYPE_PATTERNS = {
9+
// Matches: application/json, application/ld+json, application/vnd.api+json, text/json, etc.
10+
JSON: /^[\w\-]+\/([\w\-]+\+)?json$/,
11+
// Matches: application/xml, text/xml, application/atom+xml, application/rss+xml, etc.
12+
XML: /^[\w\-]+\/([\w\-]+\+)?xml$/,
13+
// Matches: text/html, application/xhtml+xml
14+
HTML: /^[\w\-]+\/([\w\-]+\+)?html$/
15+
};
16+
617
const ensureUrl = (url) => {
718
// removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
819
return url.replace(/([^:])\/{2,}/g, '$1/');
@@ -77,14 +88,28 @@ const getStatusText = (statusCode) => {
7788
return statusTexts[statusCode] || 'Unknown';
7889
};
7990

91+
/**
92+
* Determines the body type based on content-type from OpenAPI spec
93+
* Uses pattern matching to handle various MIME type variants (e.g., application/ld+json, application/vnd.api+json)
94+
* @param {string} contentType - The content-type from OpenAPI spec (object key, e.g., "application/json")
95+
* @returns {string} - The body type (json, xml, html, text)
96+
*/
8097
const getBodyTypeFromContentType = (contentType) => {
81-
if (contentType?.includes('application/json')) {
98+
if (!contentType || typeof contentType !== 'string') {
99+
return 'text';
100+
}
101+
102+
// Normalize: lowercase (object keys may vary in case, but shouldn't have parameters or whitespace)
103+
const normalizedContentType = contentType.toLowerCase();
104+
105+
if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) {
82106
return 'json';
83-
} else if (contentType?.includes('application/xml') || contentType?.includes('text/xml')) {
107+
} else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) {
84108
return 'xml';
85-
} else if (contentType?.includes('text/html')) {
109+
} else if (CONTENT_TYPE_PATTERNS.HTML.test(normalizedContentType)) {
86110
return 'html';
87111
}
112+
88113
return 'text';
89114
};
90115

@@ -163,44 +188,50 @@ const getExampleFromSchema = (schema) => {
163188

164189
/**
165190
* Populates request body in Bruno example from a value
166-
* @param {Object} body - The Bruno request body object to populate
167-
* @param {*} requestBodyValue - The request body value to set
168-
* @param {string} contentType - Content type (e.g., 'application/json')
191+
* Uses pattern matching to handle various MIME type variants
192+
* @param {Object} params - Parameters object
193+
* @param {Object} params.body - The Bruno request body object to populate
194+
* @param {*} params.requestBodyValue - The request body value to set
195+
* @param {string} params.contentType - Content type (e.g., 'application/json', 'application/ld+json')
169196
*/
170-
const populateRequestBody = (body, requestBodyValue, contentType) => {
171-
if (!requestBodyValue) return;
197+
const populateRequestBody = ({ body, requestBodyValue, contentType }) => {
198+
if (!requestBodyValue || !contentType) return;
172199

173-
if (contentType?.includes('application/json')) {
200+
// Normalize: lowercase (content types from OpenAPI spec object keys may vary in case)
201+
const normalizedContentType = contentType.toLowerCase();
202+
203+
if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) {
174204
body.mode = 'json';
175205
body.json = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue, null, 2) : requestBodyValue;
176-
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
206+
} else if (normalizedContentType === 'application/x-www-form-urlencoded') {
177207
body.mode = 'formUrlEncoded';
178208
// Handle form data if needed
179-
} else if (contentType?.includes('multipart/form-data')) {
209+
} else if (normalizedContentType === 'multipart/form-data') {
180210
body.mode = 'multipartForm';
181211
// Handle multipart form data if needed
182-
} else if (contentType?.includes('text/plain')) {
212+
} else if (normalizedContentType === 'text/plain') {
183213
body.mode = 'text';
184214
body.text = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue);
185-
} else if (contentType?.includes('text/xml') || contentType?.includes('application/xml')) {
215+
} else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) {
186216
body.mode = 'xml';
187217
body.xml = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue);
188218
}
189219
};
190220

191221
/**
192222
* Creates a Bruno example from OpenAPI example data
193-
* @param {Object} brunoRequestItem - The base Bruno request item
194-
* @param {*} exampleValue - The example value (object, array, or primitive)
195-
* @param {string} exampleName - Name of the example
196-
* @param {string} exampleDescription - Description of the example
197-
* @param {string|number} statusCode - HTTP status code (for response examples)
198-
* @param {string} contentType - Content type (e.g., 'application/json')
199-
* @param {*} requestBodyValue - Optional request body value to populate in the example
200-
* @param {string} requestBodyContentType - Optional request body content type
223+
* @param {Object} params - Parameters object
224+
* @param {Object} params.brunoRequestItem - The base Bruno request item
225+
* @param {*} params.exampleValue - The example value (object, array, or primitive)
226+
* @param {string} params.exampleName - Name of the example
227+
* @param {string} params.exampleDescription - Description of the example
228+
* @param {string|number} params.statusCode - HTTP status code (for response examples)
229+
* @param {string} params.contentType - Content type (e.g., 'application/json')
230+
* @param {*} [params.requestBodyValue] - Optional request body value to populate in the example
231+
* @param {string} [params.requestBodyContentType] - Optional request body content type
201232
* @returns {Object} Bruno example object
202233
*/
203-
const createBrunoExample = (brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodyValue = null, requestBodyContentType = null) => {
234+
const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodyValue = null, requestBodyContentType = null }) => {
204235
const brunoExample = {
205236
uid: uuid(),
206237
itemUid: brunoRequestItem.uid,
@@ -235,7 +266,7 @@ const createBrunoExample = (brunoRequestItem, exampleValue, exampleName, example
235266

236267
// Populate request body if provided
237268
if (requestBodyValue !== null) {
238-
populateRequestBody(brunoExample.request.body, requestBodyValue, requestBodyContentType);
269+
populateRequestBody({ body: brunoExample.request.body, requestBodyValue, contentType: requestBodyContentType });
239270
}
240271

241272
return brunoExample;
@@ -448,7 +479,11 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
448479
let mimeType = Object.keys(content)[0];
449480
let body = content[mimeType] || {};
450481
let bodySchema = body.schema;
451-
if (mimeType === 'application/json') {
482+
483+
// Normalize: lowercase (object keys may vary in case)
484+
const normalizedMimeType = mimeType.toLowerCase();
485+
486+
if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedMimeType)) {
452487
brunoRequestItem.request.body.mode = 'json';
453488
if (bodySchema && bodySchema.type === 'object') {
454489
let _jsonBody = buildEmptyJsonBody(bodySchema);
@@ -457,7 +492,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
457492
if (bodySchema && bodySchema.type === 'array') {
458493
brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
459494
}
460-
} else if (mimeType === 'application/x-www-form-urlencoded') {
495+
} else if (normalizedMimeType === 'application/x-www-form-urlencoded') {
461496
brunoRequestItem.request.body.mode = 'formUrlEncoded';
462497
if (bodySchema && bodySchema.type === 'object') {
463498
each(bodySchema.properties || {}, (prop, name) => {
@@ -470,7 +505,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
470505
});
471506
});
472507
}
473-
} else if (mimeType === 'multipart/form-data') {
508+
} else if (normalizedMimeType === 'multipart/form-data') {
474509
brunoRequestItem.request.body.mode = 'multipartForm';
475510
if (bodySchema && bodySchema.type === 'object') {
476511
each(bodySchema.properties || {}, (prop, name) => {
@@ -484,10 +519,10 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
484519
});
485520
});
486521
}
487-
} else if (mimeType === 'text/plain') {
522+
} else if (normalizedMimeType === 'text/plain') {
488523
brunoRequestItem.request.body.mode = 'text';
489524
brunoRequestItem.request.body.text = '';
490-
} else if (mimeType === 'text/xml') {
525+
} else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedMimeType)) {
491526
brunoRequestItem.request.body.mode = 'xml';
492527
brunoRequestItem.request.body.xml = '';
493528
}
@@ -523,14 +558,15 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
523558

524559
/**
525560
* Helper function to create examples with appropriate request body handling
526-
* @param {*} responseExampleValue - The response example value
527-
* @param {string} exampleName - Name of the example
528-
* @param {string} exampleDescription - Description of the example
529-
* @param {string|number} statusCode - HTTP status code
530-
* @param {string} responseContentType - Response content type
531-
* @param {string} [responseExampleKey] - Optional response example key for matching
561+
* @param {Object} params - Parameters object
562+
* @param {*} params.responseExampleValue - The response example value
563+
* @param {string} params.exampleName - Name of the example
564+
* @param {string} params.exampleDescription - Description of the example
565+
* @param {string|number} params.statusCode - HTTP status code
566+
* @param {string} params.responseContentType - Response content type
567+
* @param {string} [params.responseExampleKey] - Optional response example key for matching
532568
*/
533-
const createExamplesWithRequestBody = (responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, responseExampleKey = null) => {
569+
const createExamplesWithRequestBody = ({ responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, responseExampleKey = null }) => {
534570
const requestBodyExamplesWithKeys = requestBodyExamples.filter((rb) => rb.key !== null);
535571
const requestBodyExamplesWithoutKeys = requestBodyExamples.filter((rb) => rb.key === null);
536572

@@ -541,21 +577,55 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
541577

542578
if (matchingRequestBodyExample) {
543579
// Use the matching request body example
544-
examples.push(createBrunoExample(brunoRequestItem, responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, matchingRequestBodyExample.value, matchingRequestBodyExample.contentType));
580+
examples.push(createBrunoExample({
581+
brunoRequestItem,
582+
exampleValue: responseExampleValue,
583+
exampleName,
584+
exampleDescription,
585+
statusCode,
586+
contentType: responseContentType,
587+
requestBodyValue: matchingRequestBodyExample.value,
588+
requestBodyContentType: matchingRequestBodyExample.contentType
589+
}));
545590
} else if (requestBodyExamplesWithKeys.length > 0) {
546591
// No match found, create all combinations with request body examples that have keys
547592
requestBodyExamplesWithKeys.forEach((rbExample) => {
548593
const combinedExampleName = `${exampleName} (${rbExample.summary || rbExample.key})`;
549594
const combinedExampleDescription = exampleDescription || rbExample.description || '';
550-
examples.push(createBrunoExample(brunoRequestItem, responseExampleValue, combinedExampleName, combinedExampleDescription, statusCode, responseContentType, rbExample.value, rbExample.contentType));
595+
examples.push(createBrunoExample({
596+
brunoRequestItem,
597+
exampleValue: responseExampleValue,
598+
exampleName: combinedExampleName,
599+
exampleDescription: combinedExampleDescription,
600+
statusCode,
601+
contentType: responseContentType,
602+
requestBodyValue: rbExample.value,
603+
requestBodyContentType: rbExample.contentType
604+
}));
551605
});
552606
} else if (requestBodyExamplesWithoutKeys.length > 0) {
553607
// Single example or schema - use the first one for all response examples
554608
const rbExample = requestBodyExamplesWithoutKeys[0];
555-
examples.push(createBrunoExample(brunoRequestItem, responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, rbExample.value, rbExample.contentType));
609+
examples.push(createBrunoExample({
610+
brunoRequestItem,
611+
exampleValue: responseExampleValue,
612+
exampleName,
613+
exampleDescription,
614+
statusCode,
615+
contentType: responseContentType,
616+
requestBodyValue: rbExample.value,
617+
requestBodyContentType: rbExample.contentType
618+
}));
556619
} else {
557620
// No request body, create example without request body
558-
examples.push(createBrunoExample(brunoRequestItem, responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType));
621+
examples.push(createBrunoExample({
622+
brunoRequestItem,
623+
exampleValue: responseExampleValue,
624+
exampleName,
625+
exampleDescription,
626+
statusCode,
627+
contentType: responseContentType
628+
}));
559629
}
560630
};
561631

@@ -607,23 +677,54 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
607677
const exampleDescription = example.description || '';
608678
const exampleValue = example.value !== undefined ? example.value : example;
609679

610-
createExamplesWithRequestBody(exampleValue, exampleName, exampleDescription, statusCode, contentType, exampleKey);
680+
createExamplesWithRequestBody({
681+
responseExampleValue: exampleValue,
682+
exampleName,
683+
exampleDescription,
684+
statusCode,
685+
responseContentType: contentType,
686+
responseExampleKey: exampleKey
687+
});
611688
});
612689
} else if (content.example !== undefined) {
613690
// Handle example (singular) at content level
614691
const exampleName = `${statusCode} Response`;
615692
const exampleDescription = response.description || '';
616693

617-
createExamplesWithRequestBody(content.example, exampleName, exampleDescription, statusCode, contentType);
694+
createExamplesWithRequestBody({
695+
responseExampleValue: content.example,
696+
exampleName,
697+
exampleDescription,
698+
statusCode,
699+
responseContentType: contentType
700+
});
618701
} else if (content.schema) {
619702
// Handle schema - extract or generate example from schema
620703
const exampleValue = getExampleFromSchema(content.schema);
621704
const exampleName = `${statusCode} Response`;
622705
const exampleDescription = response.description || '';
623706

624-
createExamplesWithRequestBody(exampleValue, exampleName, exampleDescription, statusCode, contentType);
707+
createExamplesWithRequestBody({
708+
responseExampleValue: exampleValue,
709+
exampleName,
710+
exampleDescription,
711+
statusCode,
712+
responseContentType: contentType
713+
});
625714
}
626715
});
716+
} else {
717+
// Handle responses without content (e.g., 204 No Content)
718+
const exampleName = `${statusCode} Response`;
719+
const exampleDescription = response.description || '';
720+
721+
createExamplesWithRequestBody({
722+
responseExampleValue: '',
723+
exampleName,
724+
exampleDescription,
725+
statusCode,
726+
responseContentType: null
727+
});
627728
}
628729
});
629730
}

0 commit comments

Comments
 (0)