@@ -3,6 +3,17 @@ import get from 'lodash/get';
33import jsyaml from 'js-yaml' ;
44import { 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 \- ] + \+ ) ? j s o n $ / ,
11+ // Matches: application/xml, text/xml, application/atom+xml, application/rss+xml, etc.
12+ XML : / ^ [ \w \- ] + \/ ( [ \w \- ] + \+ ) ? x m l $ / ,
13+ // Matches: text/html, application/xhtml+xml
14+ HTML : / ^ [ \w \- ] + \/ ( [ \w \- ] + \+ ) ? h t m l $ /
15+ } ;
16+
617const 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+ */
8097const 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