fix: openapi spec with example values (#6476)

* fix: openapi spec with example values

* fix

* fix
This commit is contained in:
Pooja
2026-01-28 16:03:00 +05:30
committed by GitHub
parent af6908e9c0
commit d975d0b642
3 changed files with 955 additions and 109 deletions

View File

@@ -117,6 +117,130 @@ const getBodyTypeFromContentType = (contentType) => {
return 'text';
};
/**
* Gets a default value for a schema based on its type, format, and constraints
* Prioritizes: explicit example > enum first value > format-specific example > type default
* @param {Object} schema - The OpenAPI schema object
* @param {Map} visited - Map to track circular references
* @returns {*} - The default value for the schema
*/
const getDefaultValueForSchema = (schema, visited = new Map()) => {
// Check for explicit example first
if (schema.example !== undefined) {
return schema.example;
}
// Check for enum and use first value
if (schema.enum && schema.enum.length > 0) {
return schema.enum[0];
}
// Handle different types
if (schema.type === 'object' || schema.properties) {
return buildEmptyJsonBody(schema, visited);
}
if (schema.type === 'array') {
// Check for array-level example
if (schema.example !== undefined) {
return schema.example;
}
if (schema.items) {
if (schema.items.type === 'object' || schema.items.properties) {
return [buildEmptyJsonBody(schema.items, visited)];
}
// For primitive arrays, get example from items
if (schema.items.example !== undefined) {
return Array.isArray(schema.items.example) ? schema.items.example : [schema.items.example];
}
// Return array with a single default primitive value
const itemDefault = getDefaultValueForSchema(schema.items, visited);
if (itemDefault !== '' && itemDefault !== 0 && itemDefault !== false) {
return [itemDefault];
}
}
return [];
}
if (schema.type === 'integer' || schema.type === 'number') {
return 0;
}
if (schema.type === 'boolean') {
return false;
}
// Default for strings and other types
return '';
};
/**
* Builds XML string from OpenAPI schema
* @param {Object} bodySchema - The OpenAPI schema object
* @returns {string} - XML string
*/
const buildXmlBody = (bodySchema) => {
if (!bodySchema) return '';
// String example = raw XML, return as-is
if (typeof bodySchema.example === 'string') {
return bodySchema.example;
}
const exampleValues = typeof bodySchema.example === 'object' ? bodySchema.example : null;
if (!bodySchema.properties && !exampleValues) return '';
const rootName = bodySchema.xml?.name || 'root';
// Build a single XML element
const buildElement = (name, prop = {}, value, indent = ' ') => {
const xmlName = prop.xml?.name || name;
if (prop.xml?.attribute) return null;
// Nested object - recurse into children
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const children = Object.entries(value)
.map(([k, v]) => buildElement(k, prop.properties?.[k] || {}, v, indent + ' '))
.filter(Boolean);
return `${indent}<${xmlName}>${children.length ? '\n' + children.join('\n') + '\n' + indent : ''}</${xmlName}>`;
}
// Object schema without value - build empty structure from schema
if (prop.type === 'object' || prop.properties) {
const children = Object.entries(prop.properties || {})
.map(([k, p]) => buildElement(k, p, undefined, indent + ' '))
.filter(Boolean);
return `${indent}<${xmlName}>${children.length ? '\n' + children.join('\n') + '\n' + indent : ''}</${xmlName}>`;
}
// Primitive value
const content = value != null ? String(value) : '';
return `${indent}<${xmlName}>${content}</${xmlName}>`;
};
// Collect attributes
const attributes = Object.entries(bodySchema.properties || {})
.filter(([, p]) => p.xml?.attribute)
.map(([name, p]) => `${p.xml?.name || name}="${exampleValues?.[name] ?? ''}"`);
// Build child elements
const entries = bodySchema.properties
? Object.entries(bodySchema.properties).map(([k, p]) => [k, p, exampleValues?.[k]])
: Object.entries(exampleValues || {}).map(([k, v]) => [k, {}, v]);
const children = entries
.map(([name, prop, value]) => buildElement(name, prop, value))
.filter(Boolean);
const attrStr = attributes.length ? ' ' + attributes.join(' ') : '';
const childrenStr = children.length ? '\n' + children.join('\n') + '\n' : '';
return `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}${attrStr}>${childrenStr}</${rootName}>`;
};
const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
// Check for circular references
if (visited.has(bodySchema)) {
@@ -128,25 +252,101 @@ const buildEmptyJsonBody = (bodySchema, visited = new Map()) => {
let _jsonBody = {};
each(bodySchema.properties || {}, (prop, name) => {
if (prop.type === 'object' || prop.properties) {
_jsonBody[name] = buildEmptyJsonBody(prop, visited);
} else if (prop.type === 'array') {
if (prop.items && (prop.items.type === 'object' || prop.items.properties)) {
_jsonBody[name] = [buildEmptyJsonBody(prop.items, visited)];
} else {
_jsonBody[name] = [];
}
} else if (prop.type === 'integer' || prop.type === 'number') {
_jsonBody[name] = 0;
} else if (prop.type === 'boolean') {
_jsonBody[name] = false;
} else {
_jsonBody[name] = '';
}
_jsonBody[name] = getDefaultValueForSchema(prop, visited);
});
return _jsonBody;
};
/**
* Body type handlers for different content types
* Each handler has:
* - match: function to test if this handler should process the mime type
* - mode: the Bruno body mode to set
* - handle: function to populate the body content
*/
const BODY_TYPE_HANDLERS = [
{
match: (mimeType) => CONTENT_TYPE_PATTERNS.JSON.test(mimeType),
mode: 'json',
handle: (body, bodySchema) => {
if (bodySchema) {
if (bodySchema.example !== undefined) {
body.json = JSON.stringify(bodySchema.example, null, 2);
} else if (bodySchema.type === 'array') {
body.json = JSON.stringify(bodySchema.items ? [buildEmptyJsonBody(bodySchema.items)] : [], null, 2);
} else {
body.json = JSON.stringify(buildEmptyJsonBody(bodySchema), null, 2);
}
}
}
},
{
match: (mimeType) => mimeType === 'application/x-www-form-urlencoded',
mode: 'formUrlEncoded',
handle: (body, bodySchema) => {
if (!bodySchema) return;
const fields = bodySchema.example || bodySchema.properties || {};
const isExample = !!bodySchema.example;
each(fields, (prop, name) => {
const value = isExample ? prop : (prop.example ?? prop.default ?? '');
body.formUrlEncoded.push({
uid: uuid(),
name,
value: value !== undefined ? String(value) : '',
description: prop.description || '',
enabled: true
});
});
}
},
{
match: (mimeType) => mimeType === 'multipart/form-data',
mode: 'multipartForm',
handle: (body, bodySchema) => {
if (!bodySchema) return;
const fields = bodySchema.example || bodySchema.properties || {};
const isExample = !!bodySchema.example;
each(fields, (prop, name) => {
const isFileField = !isExample && prop.type === 'string' && prop.format === 'binary';
const value = isFileField ? [] : isExample ? prop : (prop.example ?? prop.default ?? '');
body.multipartForm.push({
uid: uuid(),
type: isFileField ? 'file' : 'text',
name,
value: isFileField ? [] : (value !== undefined ? String(value) : ''),
description: prop.description || '',
enabled: true
});
});
}
},
{
match: (mimeType) => CONTENT_TYPE_PATTERNS.XML.test(mimeType) || mimeType === 'application/xml',
mode: 'xml',
handle: (body, bodySchema) => {
body.xml = buildXmlBody(bodySchema);
}
},
{
match: (mimeType) => mimeType === 'application/sparql-query',
mode: 'sparql',
handle: (body, bodySchema) => {
// Use example from schema if available
body.sparql = bodySchema?.example !== undefined ? String(bodySchema.example) : '';
}
},
{
match: (mimeType) => ['text/plain', 'application/octet-stream', '*/*'].includes(mimeType),
mode: 'text',
handle: (body, bodySchema) => {
// Use example from schema if available
body.text = bodySchema?.example !== undefined ? String(bodySchema.example) : '';
}
}
];
/**
* Extracts or generates an example value from an OpenAPI schema
* Handles objects, arrays, primitives, and explicit examples
@@ -191,34 +391,33 @@ const getExampleFromSchema = (schema) => {
};
/**
* Populates request body in Bruno example from a value
* Uses pattern matching to handle various MIME type variants
* Populates request body in Bruno example from schema
* Reuses BODY_TYPE_HANDLERS for consistent body generation
* @param {Object} params - Parameters object
* @param {Object} params.body - The Bruno request body object to populate
* @param {*} params.requestBodyValue - The request body value to set
* @param {Object} params.bodySchema - The OpenAPI schema for the request body
* @param {string} params.contentType - Content type (e.g., 'application/json', 'application/ld+json')
*/
const populateRequestBody = ({ body, requestBodyValue, contentType }) => {
if (!requestBodyValue || !contentType || typeof contentType !== 'string') return;
const populateRequestBody = ({ body, bodySchema, contentType }) => {
if (!contentType || typeof contentType !== 'string') return;
// Normalize: lowercase (content types from OpenAPI spec object keys may vary in case)
const normalizedContentType = contentType.toLowerCase();
if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) {
body.mode = 'json';
body.json = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue, null, 2) : requestBodyValue;
} else if (normalizedContentType === 'application/x-www-form-urlencoded') {
body.mode = 'formUrlEncoded';
// Handle form data if needed
} else if (normalizedContentType === 'multipart/form-data') {
body.mode = 'multipartForm';
// Handle multipart form data if needed
} else if (normalizedContentType === 'text/plain') {
body.mode = 'text';
body.text = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue);
} else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) {
body.mode = 'xml';
body.xml = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue);
// Find matching handler and use it (same as main request body)
const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedContentType));
if (handler) {
body.mode = handler.mode;
// Clear arrays for form-based content types to avoid duplicates
// (since the body was deep-copied from the main request)
if (normalizedContentType === 'application/x-www-form-urlencoded') {
body.formUrlEncoded = [];
} else if (normalizedContentType === 'multipart/form-data') {
body.multipartForm = [];
}
handler.handle(body, bodySchema);
}
};
@@ -231,11 +430,22 @@ const populateRequestBody = ({ body, requestBodyValue, contentType }) => {
* @param {string} params.exampleDescription - Description of the example
* @param {string|number} params.statusCode - HTTP status code (for response examples)
* @param {string} params.contentType - Content type (e.g., 'application/json')
* @param {*} [params.requestBodyValue] - Optional request body value to populate in the example
* @param {Object} [params.requestBodySchema] - Optional request body schema to populate in the example
* @param {string} [params.requestBodyContentType] - Optional request body content type
* @returns {Object} Bruno example object
*/
const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodyValue = null, requestBodyContentType = null }) => {
const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodySchema = null, requestBodyContentType = null }) => {
// Deep copy the body to avoid shared references
const bodyCopy = {
mode: brunoRequestItem.request.body.mode,
json: brunoRequestItem.request.body.json,
text: brunoRequestItem.request.body.text,
xml: brunoRequestItem.request.body.xml,
sparql: brunoRequestItem.request.body.sparql || null,
formUrlEncoded: [...(brunoRequestItem.request.body.formUrlEncoded || [])],
multipartForm: [...(brunoRequestItem.request.body.multipartForm || [])]
};
const brunoExample = {
uid: uuid(),
itemUid: brunoRequestItem.uid,
@@ -247,7 +457,7 @@ const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, examp
method: brunoRequestItem.request.method,
headers: [...brunoRequestItem.request.headers],
params: [...brunoRequestItem.request.params],
body: { ...brunoRequestItem.request.body }
body: bodyCopy
},
response: {
status: String(statusCode),
@@ -268,9 +478,9 @@ const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, examp
}
};
// Populate request body if provided
if (requestBodyValue !== null) {
populateRequestBody({ body: brunoExample.request.body, requestBodyValue, contentType: requestBodyContentType });
// Populate request body from schema if provided (reuses BODY_TYPE_HANDLERS)
if (requestBodySchema !== null) {
populateRequestBody({ body: brunoExample.request.body, bodySchema: requestBodySchema, contentType: requestBodyContentType });
}
return brunoExample;
@@ -518,56 +728,19 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
// TODO: handle allOf/anyOf/oneOf
if (_operationObject.requestBody) {
let content = get(_operationObject, 'requestBody.content', {});
let mimeType = Object.keys(content)[0];
let body = content[mimeType] || {};
let bodySchema = body.schema;
const content = get(_operationObject, 'requestBody.content', {});
const mimeType = Object.keys(content)[0];
const bodyContent = content[mimeType] || {};
const bodySchema = bodyContent.schema;
// Normalize: lowercase (object keys may vary in case)
const normalizedMimeType = typeof mimeType === 'string' ? mimeType.toLowerCase() : '';
if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedMimeType)) {
brunoRequestItem.request.body.mode = 'json';
if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) {
let _jsonBody = buildEmptyJsonBody(bodySchema);
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
}
if (bodySchema && bodySchema.type === 'array') {
brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
}
} else if (normalizedMimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) {
each(bodySchema.properties || {}, (prop, name) => {
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: name,
value: '',
description: prop.description || '',
enabled: true
});
});
}
} else if (normalizedMimeType === 'multipart/form-data') {
brunoRequestItem.request.body.mode = 'multipartForm';
if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) {
each(bodySchema.properties || {}, (prop, name) => {
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
type: 'text',
name: name,
value: '',
description: prop.description || '',
enabled: true
});
});
}
} else if (normalizedMimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = '';
} else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedMimeType)) {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = '';
// Find matching handler for this content type
const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedMimeType));
if (handler) {
brunoRequestItem.request.body.mode = handler.mode;
handler.handle(brunoRequestItem.request.body, bodySchema);
}
}
@@ -627,7 +800,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
exampleDescription,
statusCode,
contentType: responseContentType,
requestBodyValue: matchingRequestBodyExample.value,
requestBodySchema: matchingRequestBodyExample.schema,
requestBodyContentType: matchingRequestBodyExample.contentType
}));
} else if (requestBodyExamplesWithKeys.length > 0) {
@@ -642,7 +815,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
exampleDescription: combinedExampleDescription,
statusCode,
contentType: responseContentType,
requestBodyValue: rbExample.value,
requestBodySchema: rbExample.schema,
requestBodyContentType: rbExample.contentType
}));
});
@@ -656,7 +829,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
exampleDescription,
statusCode,
contentType: responseContentType,
requestBodyValue: rbExample.value,
requestBodySchema: rbExample.schema,
requestBodyContentType: rbExample.contentType
}));
} else {
@@ -677,32 +850,32 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => {
if (content.examples) {
// Multiple request body examples
Object.entries(content.examples).forEach(([exampleKey, example]) => {
const exampleValue = example.value !== undefined ? example.value : example;
requestBodyExamples.push({
key: exampleKey,
value: example.value !== undefined ? example.value : example,
schema: { example: exampleValue }, // Wrap in schema format for BODY_TYPE_HANDLERS
summary: example.summary,
description: example.description,
contentType: contentType
});
});
} else if (content.example !== undefined) {
// Single request body example - convert to unified structure
// Single request body example - wrap in schema-like object
requestBodyExamples.push({
key: null, // No key for single example
value: content.example,
key: null,
schema: { example: content.example }, // Wrap in schema format for BODY_TYPE_HANDLERS
summary: null,
description: null,
contentType: contentType
});
} else if (content.schema) {
// Schema-based request body - convert to unified structure
// Schema-based request body - pass schema directly
requestBodyExamples.push({
key: null, // No key for schema
value: getExampleFromSchema(content.schema),
key: null,
schema: content.schema,
summary: null,
description: null,
contentType: contentType,
isSchema: true
contentType: contentType
});
}
});

View File

@@ -232,3 +232,680 @@ paths:
expect(bodyJson.active).toBe(false);
});
});
describe('openapi requestBody content types', () => {
it('should handle raw body with */* content type as text', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Raw Body Test"
servers:
- url: "https://api.example.com"
paths:
/raw:
post:
summary: "Raw body endpoint"
operationId: "postRaw"
requestBody:
required: true
content:
"*/*":
schema:
type: string
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('text');
expect(request.request.body.text).toBe('');
});
it('should handle binary body with application/octet-stream as text', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Binary Body Test"
servers:
- url: "https://api.example.com"
paths:
/binary:
post:
summary: "Binary body endpoint"
operationId: "postBinary"
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('text');
expect(request.request.body.text).toBe('');
});
it('should handle XML body with application/xml content type', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "XML Body Test"
servers:
- url: "https://api.example.com"
paths:
/xml:
post:
summary: "XML body endpoint"
operationId: "postXml"
requestBody:
required: true
content:
application/xml:
schema:
type: object
properties:
name:
type: string
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('xml');
expect(request.request.body.xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(request.request.body.xml).toContain('<name></name>');
});
it('should handle XML body with text/xml content type', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Text XML Body Test"
servers:
- url: "https://api.example.com"
paths:
/xml:
post:
summary: "XML body endpoint"
operationId: "postXml"
requestBody:
required: true
content:
text/xml:
schema:
type: object
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('xml');
});
it('should handle SPARQL query with application/sparql-query content type', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "SPARQL Query Test"
servers:
- url: "https://api.example.com"
paths:
/sparql:
post:
summary: "SPARQL query endpoint"
operationId: "postSparql"
requestBody:
required: true
content:
application/sparql-query:
schema:
type: string
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('sparql');
expect(request.request.body.sparql).toBe('');
});
it('should handle text/plain content type', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Text Plain Test"
servers:
- url: "https://api.example.com"
paths:
/text:
post:
summary: "Text body endpoint"
operationId: "postText"
requestBody:
required: true
content:
text/plain:
schema:
type: string
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('text');
expect(request.request.body.text).toBe('');
});
it('should detect file fields in multipart/form-data based on format: binary', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Multipart File Detection Test"
servers:
- url: "https://api.example.com"
paths:
/upload:
post:
summary: "Upload with file and text fields"
operationId: "uploadFile"
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
description: "The file to upload"
description:
type: string
description: "File description"
userId:
type: integer
description: "User ID"
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('multipartForm');
expect(request.request.body.multipartForm.length).toBe(3);
// Find the file field
const fileField = request.request.body.multipartForm.find((f) => f.name === 'file');
expect(fileField).toBeDefined();
expect(fileField.type).toBe('file');
expect(fileField.value).toEqual([]); // File fields should have array value
// Find the text fields
const descField = request.request.body.multipartForm.find((f) => f.name === 'description');
expect(descField).toBeDefined();
expect(descField.type).toBe('text');
expect(descField.value).toBe(''); // Text fields should have string value
const userIdField = request.request.body.multipartForm.find((f) => f.name === 'userId');
expect(userIdField).toBeDefined();
expect(userIdField.type).toBe('text');
expect(userIdField.value).toBe('');
});
it('should handle JSON variants like application/ld+json', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "JSON-LD Test"
servers:
- url: "https://api.example.com"
paths:
/jsonld:
post:
summary: "JSON-LD endpoint"
operationId: "postJsonLd"
requestBody:
required: true
content:
application/ld+json:
schema:
type: object
properties:
"@context":
type: string
name:
type: string
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('json');
expect(request.request.body.json).not.toBeNull();
});
it('should handle XML variants like application/atom+xml', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Atom XML Test"
servers:
- url: "https://api.example.com"
paths:
/feed:
post:
summary: "Atom feed endpoint"
operationId: "postFeed"
requestBody:
required: true
content:
application/atom+xml:
schema:
type: object
responses:
'200':
description: "Success"
`;
const result = openApiToBruno(openApiSpec);
expect(result.items.length).toBe(1);
const request = result.items[0];
expect(request.request.body.mode).toBe('xml');
});
});
describe('openapi example request body - should match main request body handling', () => {
const bodyTypesOpenApiSpec = `
openapi: 3.1.0
info:
title: Body Types Demo API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/raw-body:
post:
summary: Raw body
requestBody:
content:
"*/*":
schema:
type: string
responses:
"200":
description: Success
/json-body:
post:
summary: JSON body
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
example: "John"
responses:
"200":
description: Success
content:
application/json:
schema:
type: object
/xml-body:
post:
summary: XML body
requestBody:
content:
application/xml:
schema:
type: object
xml:
name: Root
properties:
name:
type: string
responses:
"200":
description: Success
/multipart-body:
post:
summary: Multipart body
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
desc:
type: string
responses:
"200":
description: Success
/form-body:
post:
summary: Form body
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
query:
type: string
page:
type: integer
default: 1
responses:
"200":
description: Success
/sparql-body:
post:
summary: SPARQL body
requestBody:
content:
application/sparql-query:
schema:
type: string
example: "SELECT * WHERE { ?s ?p ?o }"
responses:
"200":
description: Success
`;
it('should match body mode between request and example for all content types', () => {
const result = openApiToBruno(bodyTypesOpenApiSpec);
const tests = [
{ name: 'Raw body', mode: 'text' },
{ name: 'JSON body', mode: 'json' },
{ name: 'XML body', mode: 'xml' },
{ name: 'Multipart body', mode: 'multipartForm' },
{ name: 'Form body', mode: 'formUrlEncoded' },
{ name: 'SPARQL body', mode: 'sparql' }
];
tests.forEach(({ name, mode }) => {
const request = result.items.find((item) => item.name === name);
expect(request.request.body.mode).toBe(mode);
expect(request.examples[0].request.body.mode).toBe(mode);
});
});
it('should generate proper XML in example (not JSON)', () => {
const result = openApiToBruno(bodyTypesOpenApiSpec);
const xmlRequest = result.items.find((item) => item.name === 'XML body');
expect(xmlRequest.examples[0].request.body.xml).toContain('<?xml');
expect(xmlRequest.examples[0].request.body.xml).toContain('<Root');
expect(xmlRequest.examples[0].request.body.xml).not.toContain('{');
});
it('should detect file fields in multipart example', () => {
const result = openApiToBruno(bodyTypesOpenApiSpec);
const multipartRequest = result.items.find((item) => item.name === 'Multipart body');
const fileField = multipartRequest.examples[0].request.body.multipartForm.find((f) => f.name === 'file');
expect(fileField.type).toBe('file');
});
it('should use default values in form example', () => {
const result = openApiToBruno(bodyTypesOpenApiSpec);
const formRequest = result.items.find((item) => item.name === 'Form body');
const pageField = formRequest.examples[0].request.body.formUrlEncoded.find((f) => f.name === 'page');
expect(pageField.value).toBe('1');
});
it('should use example and enum values from schema in request body', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Test"
servers:
- url: "https://api.example.com"
paths:
/test:
post:
summary: "Test"
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
example: "John"
status:
type: string
enum: [active, inactive]
responses:
"200":
description: "OK"
`;
const result = openApiToBruno(openApiSpec);
const bodyJson = JSON.parse(result.items[0].request.body.json);
expect(bodyJson.name).toBe('John');
expect(bodyJson.status).toBe('active');
});
it('should use schema example values in main request body (not just examples)', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Schema Example Values Test"
servers:
- url: "https://api.example.com"
paths:
/users:
post:
summary: "Create user"
requestBody:
content:
application/json:
schema:
type: object
properties:
id:
type: integer
example: 9007199254740991
name:
type: string
example: "example string"
email:
type: string
format: email
example: "user@example.com"
status:
type: string
enum: [pending, active, inactive]
createdDate:
type: string
format: date
example: "2025-01-01"
score:
type: number
example: 3.1415926535
responses:
"201":
description: "Created"
`;
const result = openApiToBruno(openApiSpec);
const request = result.items[0];
// Main request body should use example values from schema
const bodyJson = JSON.parse(request.request.body.json);
expect(bodyJson.id).toBe(9007199254740991);
expect(bodyJson.name).toBe('example string');
expect(bodyJson.email).toBe('user@example.com');
expect(bodyJson.status).toBe('pending'); // first enum value
expect(bodyJson.createdDate).toBe('2025-01-01');
expect(bodyJson.score).toBe(3.1415926535);
});
it('should handle XML body with object example (not produce [object Object])', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "XML Object Example Test"
servers:
- url: "https://api.example.com"
paths:
/user:
post:
summary: "Create user"
operationId: "createUser"
requestBody:
required: true
content:
application/xml:
schema:
type: object
example:
name: "John"
age: 30
properties:
name:
type: string
age:
type: integer
responses:
"201":
description: "Created"
`;
const result = openApiToBruno(openApiSpec);
const request = result.items[0];
expect(request.request.body.mode).toBe('xml');
// Should NOT contain [object Object]
expect(request.request.body.xml).not.toContain('[object Object]');
// Should contain the example values
expect(request.request.body.xml).toContain('<name>John</name>');
expect(request.request.body.xml).toContain('<age>30</age>');
});
it('should handle XML body with string example (raw XML)', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "XML String Example Test"
servers:
- url: "https://api.example.com"
paths:
/user:
post:
summary: "Create user"
operationId: "createUser"
requestBody:
required: true
content:
application/xml:
schema:
type: string
example: '<user><name>John</name></user>'
responses:
"201":
description: "Created"
`;
const result = openApiToBruno(openApiSpec);
const request = result.items[0];
expect(request.request.body.mode).toBe('xml');
// Should preserve the raw XML string
expect(request.request.body.xml).toBe('<user><name>John</name></user>');
});
it('should not crash when array schema has no items defined', () => {
const openApiSpec = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Array Without Items Test"
servers:
- url: "https://api.example.com"
paths:
/items:
post:
summary: "Create items"
operationId: "createItems"
requestBody:
required: true
content:
application/json:
schema:
type: array
responses:
"201":
description: "Created"
`;
// Should not throw an error
expect(() => openApiToBruno(openApiSpec)).not.toThrow();
const result = openApiToBruno(openApiSpec);
const request = result.items[0];
expect(request.request.body.mode).toBe('json');
// Should produce an empty array
expect(request.request.body.json).toBe('[]');
});
});

View File

@@ -102,16 +102,12 @@ const astRequestAttribute = {
},
requestmode(_1, _2, _3, _4, value) {
const modeValue = value.sourceString ? value.sourceString.trim() : '';
// If mode is "none", return a body with mode: "none"
if (modeValue === 'none') {
return {
body: {
mode: 'none'
}
};
}
// For other modes, return nothing since the body parser will handle it
return {};
// Return body with the mode set
return {
body: {
mode: modeValue || 'none'
}
};
},
requestparamspath(_1, _2, _3, _4, dictionary) {
return {