@usebruno/converters
Version:
The converters package is responsible for converting collections from one format to a Bruno collection. It can be used as a standalone package or as a part of the Bruno framework.
1,134 lines (1,014 loc) • 35 kB
JavaScript
// Custom UID generator for alphanumeric IDs (no hyphens)
const generateUID = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 21; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
import { get, each } from 'lodash';
import { collectionSchema } from '@usebruno/schema';
// --- Inlined from src/common/index.js ---
export const validateSchema = (collection = {}) => {
try {
collectionSchema.validateSync(collection);
return collection;
} catch (err) {
throw new Error('The Collection has an invalid schema: ' + err.message);
}
};
export const transformItemsInCollection = (collection) => {
const transformItems = (items = []) => {
each(items, (item) => {
if (['http', 'graphql'].includes(item.type)) {
item.type = `${item.type}-request`;
if (item.request.query) {
item.request.params = item.request.query.map((queryItem) => ({
...queryItem,
type: 'query',
uid: queryItem.uid || generateUID()
}));
}
delete item.request.query;
let multipartFormData = get(item, 'request.body.multipartForm');
if (multipartFormData) {
each(multipartFormData, (form) => {
if (!form.type) {
form.type = 'text';
}
});
}
}
// Handle already transformed types
if (['http-request', 'graphql-request'].includes(item.type)) {
if (item.request.query) {
item.request.params = item.request.query.map((queryItem) => ({
...queryItem,
type: 'query',
uid: queryItem.uid || generateUID()
}));
}
delete item.request.query;
let multipartFormData = get(item, 'request.body.multipartForm');
if (multipartFormData) {
each(multipartFormData, (form) => {
if (!form.type) {
form.type = 'text';
}
});
}
}
if (item.items && item.items.length) {
transformItems(item.items);
}
});
};
transformItems(collection.items);
return collection;
};
const isItemARequest = (item) => {
return ['http-request', 'graphql-request'].includes(item.type);
};
export const hydrateSeqInCollection = (collection) => {
const hydrateSeq = (items = []) => {
let index = 1;
each(items, (item) => {
if (isItemARequest(item) && !item.seq) {
item.seq = index;
index++;
}
if (item.items && item.items.length) {
hydrateSeq(item.items);
}
});
};
hydrateSeq(collection.items);
return collection;
};
// --- End inlined ---
// Use a simple XML parser for Node.js environment
const parseXML = (xmlString) => {
const parser = new (require('xml2js')).Parser({
explicitArray: false,
ignoreAttrs: false,
mergeAttrs: true,
xmlns: false
});
return new Promise((resolve, reject) => {
parser.parseString(xmlString, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
};
const addSuffixToDuplicateName = (item, index, allItems) => {
// Check if the request name already exist and if so add a number suffix
const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {
if (otherItem.name === item.name && otherIndex < index) {
nameSuffix++;
}
return nameSuffix;
}, 0);
return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;
};
/**
* Enhanced WSDL Parser based on wizdler approach
*/
class WSDLParser {
constructor() {
this.types = new Map();
this.elements = new Map();
this.complexTypes = new Map();
this.simpleTypes = new Map();
this.messages = new Map();
this.portTypes = new Map();
this.bindings = new Map();
this.services = new Map();
this.namespaces = new Map();
}
/**
* Parse WSDL content and extract all components
*/
async parse(wsdlContent) {
const result = await parseXML(wsdlContent);
const definitions = result['wsdl:definitions'] || result.definitions;
if (!definitions) {
throw new Error('No definitions found in WSDL');
}
// Extract namespaces
this.extractNamespaces(definitions);
// Parse types (XSD schemas)
if (definitions['wsdl:types'] || definitions.types) {
this.parseTypes(definitions['wsdl:types'] || definitions.types);
}
// Parse messages
this.parseMessages(definitions);
// Parse port types
this.parsePortTypes(definitions);
// Parse bindings
this.parseBindings(definitions);
// Parse services
this.parseServices(definitions);
return {
targetNamespace: definitions.targetNamespace || '',
name: definitions.name || 'WSDL Service',
types: this.types,
elements: this.elements,
complexTypes: this.complexTypes,
simpleTypes: this.simpleTypes,
messages: this.messages,
portTypes: this.portTypes,
bindings: this.bindings,
services: this.services,
namespaces: this.namespaces
};
}
/**
* Extract all namespaces from WSDL
*/
extractNamespaces(definitions) {
// Extract from xmlns attributes
for (const [key, value] of Object.entries(definitions)) {
if (key.startsWith('xmlns:')) {
const prefix = key.substring(6);
this.namespaces.set(prefix, value);
} else if (key === 'xmlns') {
this.namespaces.set('', value);
}
}
}
/**
* Parse WSDL types section (XSD schemas)
*/
parseTypes(typesNode) {
if (!typesNode) return;
const schemas = this.getArray(typesNode['xsd:schema'] || typesNode.schema);
for (const schema of schemas) {
const targetNamespace = schema.targetNamespace || '';
// Parse complex types FIRST (so they can be referenced by elements)
const complexTypes = this.getArray(schema['xsd:complexType'] || schema.complexType);
for (const complexType of complexTypes) {
this.parseComplexType(complexType, targetNamespace);
}
// Parse simple types
const simpleTypes = this.getArray(schema['xsd:simpleType'] || schema.simpleType);
for (const simpleType of simpleTypes) {
this.parseSimpleType(simpleType, targetNamespace);
}
// Parse elements LAST (so they can reference complex types)
const elements = this.getArray(schema['xsd:element'] || schema.element);
for (const element of elements) {
this.parseElement(element, targetNamespace);
}
}
}
/**
* Parse an XSD element
*/
parseElement(element, namespace) {
const key = `${namespace}:${element.name}`;
const parsedElement = {
name: element.name,
namespace: namespace,
type: element.type,
minOccurs: element.minOccurs,
maxOccurs: element.maxOccurs,
nillable: element.nillable,
form: element.form,
attributes: [],
elements: []
};
// Handle inline complex type (recursively parse children)
if (element['xsd:complexType'] || element.complexType) {
const complexType = element['xsd:complexType'] || element.complexType;
// Recursively parse sequence/choice/all children as elements
if (complexType['xsd:sequence'] || complexType.sequence) {
const sequence = complexType['xsd:sequence'] || complexType.sequence;
const children = this.getArray(sequence['xsd:element'] || sequence.element);
for (const child of children) {
// Recursively parse child element
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
if (complexType['xsd:choice'] || complexType.choice) {
const choice = complexType['xsd:choice'] || complexType.choice;
const children = this.getArray(choice['xsd:element'] || choice.element);
for (const child of children) {
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
if (complexType['xsd:all'] || complexType.all) {
const all = complexType['xsd:all'] || complexType.all;
const children = this.getArray(all['xsd:element'] || all.element);
for (const child of children) {
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
// Parse attributes
if (complexType['xsd:attribute'] || complexType.attribute) {
const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
for (const attr of attributes) {
parsedElement.attributes.push({
name: attr.name,
type: attr.type,
use: attr.use,
default: attr.default,
fixed: attr.fixed,
form: attr.form
});
}
}
}
// Handle inline simple type
if (element['xsd:simpleType'] || element.simpleType) {
const simpleType = element['xsd:simpleType'] || element.simpleType;
parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);
}
// Handle referenced complex type - resolve it immediately
if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {
const typeName = element.type.replace(/^.*:/, '');
const complexType = this.findComplexTypeByName(typeName, namespace);
if (complexType) {
parsedElement.elements = complexType.elements || [];
parsedElement.attributes = complexType.attributes || [];
}
}
this.elements.set(key, parsedElement);
return parsedElement; // for inline recursion
}
/**
* Helper for parsing inline child elements (does not add to elements map)
*/
parseElementInline(element, namespace) {
const parsedElement = {
name: element.name,
namespace: namespace,
type: element.type,
minOccurs: element.minOccurs,
maxOccurs: element.maxOccurs,
nillable: element.nillable,
form: element.form,
attributes: [],
elements: []
};
// Inline complex type
if (element['xsd:complexType'] || element.complexType) {
const complexType = element['xsd:complexType'] || element.complexType;
if (complexType['xsd:sequence'] || complexType.sequence) {
const sequence = complexType['xsd:sequence'] || complexType.sequence;
const children = this.getArray(sequence['xsd:element'] || sequence.element);
for (const child of children) {
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
if (complexType['xsd:choice'] || complexType.choice) {
const choice = complexType['xsd:choice'] || complexType.choice;
const children = this.getArray(choice['xsd:element'] || choice.element);
for (const child of children) {
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
if (complexType['xsd:all'] || complexType.all) {
const all = complexType['xsd:all'] || complexType.all;
const children = this.getArray(all['xsd:element'] || all.element);
for (const child of children) {
parsedElement.elements.push(this.parseElementInline(child, namespace));
}
}
// Parse attributes
if (complexType['xsd:attribute'] || complexType.attribute) {
const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
for (const attr of attributes) {
parsedElement.attributes.push({
name: attr.name,
type: attr.type,
use: attr.use,
default: attr.default,
fixed: attr.fixed,
form: attr.form
});
}
}
}
// Inline simple type
if (element['xsd:simpleType'] || element.simpleType) {
const simpleType = element['xsd:simpleType'] || element.simpleType;
parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);
}
// Referenced complex type
if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {
const typeName = element.type.replace(/^.*:/, '');
const complexType = this.findComplexTypeByName(typeName, namespace);
if (complexType) {
parsedElement.elements = complexType.elements || [];
parsedElement.attributes = complexType.attributes || [];
}
}
return parsedElement;
}
/**
* Find complex type by name and namespace
*/
findComplexTypeByName(typeName, namespace) {
// Try with namespace
const key = `${namespace}:${typeName}`;
if (this.complexTypes.has(key)) {
return this.complexTypes.get(key);
}
// Try without namespace
for (const [key, complexType] of this.complexTypes) {
if (complexType.name === typeName) {
return complexType;
}
}
return null;
}
/**
* Parse an XSD complex type
*/
parseComplexType(complexType, namespace) {
const key = `${namespace}:${complexType.name}`;
const parsedComplexType = {
name: complexType.name,
namespace: namespace,
attributes: [],
elements: [],
mixed: complexType.mixed,
abstract: complexType.abstract
};
this.parseComplexTypeContent(complexType, parsedComplexType);
this.complexTypes.set(key, parsedComplexType);
}
/**
* Parse complex type content (sequence, choice, all, attributes)
*/
parseComplexTypeContent(complexType, target) {
// Parse sequence
if (complexType['xsd:sequence'] || complexType.sequence) {
const sequence = complexType['xsd:sequence'] || complexType.sequence;
this.parseSequence(sequence, target);
}
// Parse choice
if (complexType['xsd:choice'] || complexType.choice) {
const choice = complexType['xsd:choice'] || complexType.choice;
this.parseChoice(choice, target);
}
// Parse all
if (complexType['xsd:all'] || complexType.all) {
const all = complexType['xsd:all'] || complexType.all;
this.parseAll(all, target);
}
// Parse attributes
if (complexType['xsd:attribute'] || complexType.attribute) {
const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);
for (const attr of attributes) {
target.attributes.push({
name: attr.name,
type: attr.type,
use: attr.use,
default: attr.default,
fixed: attr.fixed,
form: attr.form
});
}
}
// Handle simple content with extension
if (complexType['xsd:simpleContent'] || complexType.simpleContent) {
const simpleContent = complexType['xsd:simpleContent'] || complexType.simpleContent;
if (simpleContent['xsd:extension'] || simpleContent.extension) {
const extension = simpleContent['xsd:extension'] || simpleContent.extension;
target.baseType = extension.base;
// Parse attributes from extension
if (extension['xsd:attribute'] || extension.attribute) {
const attributes = this.getArray(extension['xsd:attribute'] || extension.attribute);
for (const attr of attributes) {
target.attributes.push({
name: attr.name,
type: attr.type,
use: attr.use,
default: attr.default,
fixed: attr.fixed,
form: attr.form
});
}
}
}
}
// Handle complex content with extension
if (complexType['xsd:complexContent'] || complexType.complexContent) {
const complexContent = complexType['xsd:complexContent'] || complexType.complexContent;
if (complexContent['xsd:extension'] || complexContent.extension) {
const extension = complexContent['xsd:extension'] || complexContent.extension;
target.baseType = extension.base;
// Parse content from extension
this.parseComplexTypeContent(extension, target);
}
}
}
/**
* Parse sequence content
*/
parseSequence(sequence, target) {
const elements = this.getArray(sequence['xsd:element'] || sequence.element);
for (const element of elements) {
// Use parseElementInline to properly handle inline complex types and attributes
const parsedElement = this.parseElementInline(element, target.namespace || '');
target.elements.push(parsedElement);
}
}
/**
* Parse choice content
*/
parseChoice(choice, target) {
const elements = this.getArray(choice['xsd:element'] || choice.element);
for (const element of elements) {
// Use parseElementInline to properly handle inline complex types and attributes
const parsedElement = this.parseElementInline(element, target.namespace || '');
parsedElement.choice = true;
target.elements.push(parsedElement);
}
}
/**
* Parse all content
*/
parseAll(all, target) {
const elements = this.getArray(all['xsd:element'] || all.element);
for (const element of elements) {
// Use parseElementInline to properly handle inline complex types and attributes
const parsedElement = this.parseElementInline(element, target.namespace || '');
parsedElement.all = true;
target.elements.push(parsedElement);
}
}
/**
* Parse simple type
*/
parseSimpleType(simpleType, namespace) {
const key = `${namespace}:${simpleType.name}`;
const parsedSimpleType = {
name: simpleType.name,
namespace: namespace,
...this.parseSimpleTypeContent(simpleType)
};
this.simpleTypes.set(key, parsedSimpleType);
}
/**
* Parse simple type content
*/
parseSimpleTypeContent(simpleType) {
if (simpleType['xsd:restriction'] || simpleType.restriction) {
const restriction = simpleType['xsd:restriction'] || simpleType.restriction;
return {
base: restriction.base,
enumeration: this.getArray(restriction['xsd:enumeration'] || restriction.enumeration),
pattern: restriction['xsd:pattern'] || restriction.pattern,
minLength: restriction['xsd:minLength'] || restriction.minLength,
maxLength: restriction['xsd:maxLength'] || restriction.maxLength
};
}
return {};
}
/**
* Parse WSDL messages
*/
parseMessages(definitions) {
const messages = this.getArray(definitions['wsdl:message'] || definitions.message);
for (const message of messages) {
const parts = this.getArray(message['wsdl:part'] || message.part);
this.messages.set(message.name, {
name: message.name,
parts: parts.map((part) => ({
name: part.name,
type: part.type,
element: part.element
}))
});
}
}
/**
* Parse WSDL port types
*/
parsePortTypes(definitions) {
const portTypes = this.getArray(definitions['wsdl:portType'] || definitions.portType);
for (const portType of portTypes) {
const operations = this.getArray(portType['wsdl:operation'] || portType.operation);
this.portTypes.set(portType.name, {
name: portType.name,
operations: operations.map((op) => ({
name: op.name,
input: op['wsdl:input'] || op.input,
output: op['wsdl:output'] || op.output,
fault: this.getArray(op['wsdl:fault'] || op.fault)
}))
});
}
}
/**
* Parse WSDL bindings
*/
parseBindings(definitions) {
const bindings = this.getArray(definitions['wsdl:binding'] || definitions.binding);
for (const binding of bindings) {
const operations = this.getArray(binding['wsdl:operation'] || binding.operation);
this.bindings.set(binding.name, {
name: binding.name,
type: binding.type,
operations: operations.map((op) => {
// Robustly extract soapAction from any soap:operation child element
let soapAction = '';
for (const key of Object.keys(op)) {
if (key.endsWith(':operation')) {
const soapOp = op[key];
if (Array.isArray(soapOp)) {
if (soapOp[0] && soapOp[0].soapAction) {
soapAction = soapOp[0].soapAction;
break;
}
} else if (soapOp && soapOp.soapAction) {
soapAction = soapOp.soapAction;
break;
}
}
}
return {
name: op.name,
input: op['wsdl:input'] || op.input,
output: op['wsdl:output'] || op.output,
fault: this.getArray(op['wsdl:fault'] || op.fault),
soapAction: soapAction
};
})
});
}
}
/**
* Parse WSDL services
*/
parseServices(definitions) {
const services = this.getArray(definitions['wsdl:service'] || definitions.service);
for (const service of services) {
const ports = this.getArray(service['wsdl:port'] || service.port);
this.services.set(service.name, {
name: service.name,
ports: ports.map((port) => ({
name: port.name,
binding: port.binding,
address: this.extractAddress(port)
}))
});
}
}
/**
* Extract service address from port
*/
extractAddress(port) {
// Try different address formats
const address = port['soap:address'] || port['wsdl:address'] || port.address;
if (address && address.location) {
return address.location;
}
return '';
}
/**
* Helper to ensure array
*/
getArray(item) {
if (!item) return [];
return Array.isArray(item) ? item : [item];
}
}
/**
* Enhanced XML Sample Generator based on wizdler approach
*/
class XMLSampleGenerator {
constructor(wsdlData) {
this.wsdlData = wsdlData;
this.visitedTypes = new Set();
}
/**
* Generate XML sample for an element
*/
generateSample(elementName, namespace = '') {
const element = this.findElement(elementName, namespace);
if (!element) {
return `<!-- Element ${elementName} not found -->`;
}
return this.generateElementSample(element, 0);
}
/**
* Find element by name and namespace
*/
findElement(elementName, namespace) {
// Try with namespace
if (namespace) {
const key = `${namespace}:${elementName}`;
if (this.wsdlData.elements.has(key)) {
return this.wsdlData.elements.get(key);
}
}
// Try without namespace
for (const [key, element] of this.wsdlData.elements) {
if (element.name === elementName) {
return element;
}
}
return null;
}
/**
* Generate sample for an element
*/
generateElementSample(element) {
let xml = '';
// Add comments for optional/repetition elements
const minOccurs = parseInt(element.minOccurs) || 1;
const maxOccurs = element.maxOccurs || '1';
if (minOccurs === 0) {
xml += `<!--Optional:-->`;
}
if (maxOccurs === 'unbounded' || (typeof maxOccurs === 'number' && maxOccurs > 1)) {
xml += `<!--${this.getRepetitionText(minOccurs, maxOccurs)}-->`;
}
// Generate attributes
const attributes = this.generateAttributes(element);
// Generate element content
if (this.isSimpleType(element)) {
xml += `<${element.name}${attributes}>${this.getSampleValue(element)}</${element.name}>`;
} else {
xml += `<${element.name}${attributes}>`;
xml += this.generateComplexContent(element);
xml += `</${element.name}>`;
}
return xml;
}
/**
* Recursively collect all attributes from a complex type and its base types
*/
collectAllAttributes(complexType) {
let attributes = [];
if (complexType && complexType.attributes) {
attributes = attributes.concat(complexType.attributes);
}
// Recursively collect from base type if present
if (complexType && complexType.baseType) {
const baseTypeName = complexType.baseType.replace(/^.*:/, '');
const baseType = this.findComplexType(baseTypeName);
if (baseType) {
attributes = attributes.concat(this.collectAllAttributes(baseType));
}
}
return attributes;
}
/**
* Generate attributes string
*/
generateAttributes(element) {
let attributes = [];
// Add attributes from the element itself
if (element.attributes && element.attributes.length > 0) {
attributes = attributes.concat(element.attributes);
}
// Add attributes from the referenced complex type (if any, recursively)
if (element.type) {
const complexType = this.findComplexType(element.type);
if (complexType) {
const allTypeAttrs = this.collectAllAttributes(complexType);
// Avoid duplicates by attribute name
const existingNames = new Set(attributes.map((a) => a.name));
for (const attr of allTypeAttrs) {
if (!existingNames.has(attr.name)) {
attributes.push(attr);
}
}
}
}
if (attributes.length > 0) {
return ' ' + attributes.map((attr) => `${attr.name}="?"`).join(' ');
}
return '';
}
/**
* Check if element is simple type
*/
isSimpleType(element) {
if (element.simpleType) return true;
const type = element.type;
if (!type) return false;
// Check if it's a built-in simple type
const simpleTypes = [
'string', 'int', 'integer', 'long', 'short', 'byte', 'boolean', 'float', 'double', 'decimal',
'date', 'dateTime', 'time', 'duration', 'gYear', 'gYearMonth', 'gMonth', 'gMonthDay', 'gDay',
'hexBinary', 'base64Binary', 'anyURI', 'QName', 'NOTATION', 'normalizedString', 'token',
'language', 'Name', 'NCName', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'
];
const typeName = type.replace(/^.*:/, '');
return simpleTypes.includes(typeName);
}
/**
* Get sample value for simple type
*/
getSampleValue(element) {
if (element.simpleType && element.simpleType.enumeration && element.simpleType.enumeration.length > 0) {
return element.simpleType.enumeration[0].value || '?';
}
const type = element.type;
if (!type) return '?';
const typeName = type.replace(/^.*:/, '');
switch (typeName) {
case 'string': return 'string';
case 'int':
case 'integer':
case 'long':
case 'short':
case 'byte': return '0';
case 'boolean': return 'true';
case 'float':
case 'double':
case 'decimal': return '0.0';
case 'date': return '2024-01-01';
case 'dateTime': return '2024-01-01T00:00:00Z';
case 'time': return '00:00:00';
default: return '?';
}
}
/**
* Generate complex content
*/
generateComplexContent(element) {
let xml = '';
// Handle inline complex type (elements already parsed)
if (element.elements && element.elements.length > 0) {
for (const child of element.elements) {
xml += this.generateElementSample(child);
}
}
// Handle referenced complex type - this is the key fix
if (element.type) {
const complexType = this.findComplexType(element.type);
if (complexType) {
xml += this.generateComplexTypeSample(complexType);
} else {
// If we can't find the complex type, try to find it as an element
const elementType = this.findElement(element.type.replace(/^.*:/, ''), '');
if (elementType) {
xml += this.generateElementSample(elementType);
}
}
}
return xml;
}
/**
* Find complex type by name
*/
findComplexType(typeName) {
const cleanTypeName = typeName.replace(/^.*:/, '');
// First try exact match
for (const [key, complexType] of this.wsdlData.complexTypes) {
if (complexType.name === cleanTypeName) {
return complexType;
}
}
// Try with namespace prefix
for (const [key, complexType] of this.wsdlData.complexTypes) {
if (key.endsWith(`:${cleanTypeName}`) || key === cleanTypeName) {
return complexType;
}
}
return null;
}
/**
* Generate sample for complex type
*/
generateComplexTypeSample(complexType) {
if (this.visitedTypes.has(complexType.name)) {
return '<!-- Recursive type detected -->';
}
this.visitedTypes.add(complexType.name);
let xml = '';
if (complexType.elements && complexType.elements.length > 0) {
for (const element of complexType.elements) {
xml += this.generateElementSample(element);
}
}
this.visitedTypes.delete(complexType.name);
return xml;
}
/**
* Get repetition text
*/
getRepetitionText(minOccurs, maxOccurs) {
if (minOccurs === 0 && maxOccurs === 'unbounded') {
return '0 or more repetitions';
} else if (minOccurs === 1 && maxOccurs === 'unbounded') {
return '1 or more repetitions';
} else if (typeof maxOccurs === 'number') {
return `${minOccurs} to ${maxOccurs} repetitions:`;
} else {
return '0 or more repetitions';
}
}
}
/**
* Generate SOAP envelope with example payload
*/
const generateSOAPEnvelope = (operation, wsdlData) => {
const inputMessage = operation.input?.message || '';
const inputMessageName = typeof inputMessage === 'string' && inputMessage.includes(':') ? inputMessage.split(':')[1] : inputMessage;
// Find the message definition
const message = wsdlData.messages.get(inputMessageName);
if (!message || !message.parts || message.parts.length === 0) {
return '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><!-- No message parts found --></soap:Body></soap:Envelope>';
}
const part = message.parts[0];
const elementName = part.element || part.type || '';
if (!elementName) {
return '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><!-- No element found --></soap:Body></soap:Envelope>';
}
// Extract element name and namespace
let name, namespace;
if (elementName.includes(':')) {
[namespace, name] = elementName.split(':');
} else {
name = elementName;
namespace = '';
}
// Generate XML sample
const generator = new XMLSampleGenerator(wsdlData);
const xmlSample = generator.generateSample(name, namespace);
return `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body>${xmlSample}</soap:Body></soap:Envelope>`;
};
/**
* Transform WSDL operation to Bruno request item
*/
const transformWSDLOperation = (operation, wsdlData, serviceLocation, index, allOperations, bindingOperation = null) => {
// Create a temporary object with the name property for duplicate checking
const tempItem = { name: operation.name };
const name = addSuffixToDuplicateName(tempItem, index, allOperations);
const soapEnvelope = generateSOAPEnvelope(operation, wsdlData);
// Use soapAction from binding operation if available, otherwise fallback to constructed value
let soapAction = '';
if (bindingOperation && bindingOperation.soapAction) {
soapAction = bindingOperation.soapAction;
} else {
// Fallback to constructed value
soapAction = `"${wsdlData.targetNamespace || ''}${operation.name}"`;
}
const brunoRequestItem = {
uid: generateUID(),
name,
type: 'http-request',
request: {
url: serviceLocation || '',
method: 'POST',
auth: {
mode: 'none',
basic: null,
bearer: null,
digest: null
},
headers: [
{
uid: generateUID(),
name: 'Content-Type',
value: 'text/xml; charset=utf-8',
description: '',
enabled: true
},
{
uid: generateUID(),
name: 'SOAPAction',
value: soapAction,
description: '',
enabled: true
}
],
params: [],
body: {
mode: 'xml',
json: null,
text: null,
xml: soapEnvelope,
formUrlEncoded: [],
multipartForm: []
},
script: {
res: null
}
}
};
return brunoRequestItem;
};
/**
* Parse WSDL collection and transform to Bruno format
*/
const parseWSDLCollection = (wsdlData) => {
const collection = {
uid: generateUID(),
version: '1',
name: wsdlData.name,
items: []
};
// Flatten the structure to avoid duplicate folder names
// Group operations by service and port, but create a single folder per service
for (const [serviceName, service] of wsdlData.services) {
const serviceFolder = {
uid: generateUID(),
name: serviceName,
type: 'folder',
items: []
};
// Collect all operations from all ports in this service
const allOperations = [];
for (const port of service.ports) {
// Find operations for this port
const bindingName = port.binding && typeof port.binding === 'string' && port.binding.includes(':') ? port.binding.split(':')[1] : port.binding;
const binding = wsdlData.bindings.get(bindingName);
if (binding) {
const bindingType = binding.type && typeof binding.type === 'string' && binding.type.includes(':') ? binding.type.split(':')[1] : binding.type;
const portType = wsdlData.portTypes.get(bindingType);
if (portType) {
for (const portTypeOp of portType.operations) {
// Find the corresponding binding operation by name
const bindingOp = binding.operations.find((bop) => bop.name === portTypeOp.name);
if (bindingOp) {
const request = transformWSDLOperation(portTypeOp, wsdlData, port.address, allOperations.length, binding.operations, bindingOp);
allOperations.push(request);
}
}
}
}
}
// Add all operations directly to the service folder
serviceFolder.items = allOperations;
if (serviceFolder.items.length > 0) {
collection.items.push(serviceFolder);
}
}
return collection;
};
/**
* Convert WSDL content to Bruno collection
*/
export const wsdlToBruno = async (wsdlContent) => {
try {
if (typeof wsdlContent !== 'string') {
throw new Error('WSDL content must be a string');
}
// Parse WSDL using enhanced parser
const parser = new WSDLParser();
const wsdlData = await parser.parse(wsdlContent);
const collection = parseWSDLCollection(wsdlData);
const transformedCollection = transformItemsInCollection(collection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
const validatedCollection = validateSchema(hydratedCollection);
return validatedCollection;
} catch (err) {
console.error(err);
throw new Error('Import WSDL collection failed: ' + err.message);
}
};
export { WSDLParser, XMLSampleGenerator };
export default wsdlToBruno;