@redocly/portal-plugin-soap-api
Version:
SOAP API plugin for Redocly products
275 lines (240 loc) • 7.83 kB
text/typescript
import { WSDL, Client } from 'soap';
import formatXml from 'xml-formatter';
import { fileURLToPath } from 'url';
import path, { resolve, join } from 'node:path';
import type { ExternalPlugin } from '@redocly/realm/dist/server/types';
import type { Logger } from '@redocly/realm/dist/server/utils/reporter/logger';
import type { Feature } from '@redocly/realm/dist/types';
import type { RedoclyConfig } from '@redocly/config';
import { searchResolver } from './search-resolver.js';
const SOAP_TEMPLATE_ID = 'soap-docs';
const STUB_SOAP_RESPONSE = `HTTP/1.0 200 OK
Content-Type: text/xml; charset = utf-8
Content-Length: 42
<?xml version = "1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">
<SOAP-ENV:Body xmlns:m = "http://www.xyz.org/quotation">
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>`;
export default function soapPlugin(): ExternalPlugin {
return {
id: 'soap',
requiredEntitlements: ['soap' as Feature],
loaders: {
wsdl: async (relativePath, context) => {
const config = await context.getConfig(relativePath);
const { metadata } = getApiConfigByPath(config.apis, relativePath);
const content = await context.fs.read(relativePath);
const wsdl = (await new Promise((resolve) => {
const wsdl = new WSDL(content, relativePath, {});
wsdl.onReady(() => resolve(wsdl));
})) as WSDL;
const info = new Client(wsdl, undefined, {
httpClient: {
request: (
_location: any,
_data: any,
cb: (err: any, res: any, body: string) => void,
): any => {
cb(undefined, { status: 200 }, STUB_SOAP_RESPONSE);
},
},
});
const meta = info.describe();
const serviceName = Object.keys(meta)[0];
const bindingName = Object.keys(meta[serviceName])[0];
const endpoints = meta[serviceName][bindingName];
return {
endpoints,
metadata,
serviceName,
info,
content,
};
},
},
processContent: async (actions: any, context: any) => {
const soapTemplateId = actions.createTemplate(
SOAP_TEMPLATE_ID,
fromCurrentDir(import.meta.url, './template.js'),
);
for (const fileInfo of await context.fs.scan(/(\.wsdl)$/)) {
if (await context.isPathIgnored(fileInfo.relativePath)) {
continue;
}
const wsdlRecord = await context.cache.load(fileInfo.relativePath, 'wsdl');
const { endpoints, metadata, serviceName, info, content } = wsdlRecord.data;
for (const endpointName of Object.keys(endpoints ?? {})) {
/**
* Add a page for operation
*/
actions.addRoute({
fsPath: fileInfo.relativePath,
slugSuffix: slugFromEndpoint(endpointName),
templateId: soapTemplateId,
getNavText: async () => endpointName,
getSearchDocuments: searchResolver(actions),
getStaticData: async () => {
return {
props: {
title: endpointName,
meta: {},
endpointName,
request: await generateRequestExample(
endpointName,
info,
endpoints[endpointName].input,
context.logger,
),
},
};
},
});
}
/**
* Add a route with general information
*/
actions.addRoute({
fsPath: fileInfo.relativePath,
templateId: soapTemplateId,
metadata: {
type: 'soap',
title: serviceName,
description: '',
tags: ['SOAP'],
...metadata,
},
getSidebar: (route: any) => {
const operationSidebarItems = [];
for (const endpointName of Object.keys(endpoints)) {
const routeSlug = join(route.slug, slugFromEndpoint(endpointName));
operationSidebarItems.push({
type: 'link',
link: routeSlug,
label: endpointName,
routeSlug,
});
}
return [
{
type: 'link',
link: route.slug,
routeSlug: route.slug,
label: serviceName,
},
{
type: 'group',
label: 'Operations',
items: operationSidebarItems,
},
];
},
getNavText: async () => serviceName,
getSearchDocuments: searchResolver(actions),
getStaticData: async () => {
return {
props: {
title: serviceName,
wsdl: formatXml(content, { indentation: ' ' }),
},
};
},
});
}
},
};
}
/**
* function which generates an example request XML
*/
const generateRequestExample = async (
endpointName: string,
info: any,
inputParams: any,
logger: Logger,
) => {
const mockInput = mockProperties(endpointName, inputParams, 1, logger);
const requestXml = (await new Promise((resolve) => {
info[endpointName](mockInput, (...params: any[]) => {
resolve(params[4]);
});
})) as string;
return formatXml(requestXml, { indentation: ' ', throwOnFailure: false });
};
const mockProperties = (
endpointName: string,
inputParams: any,
recursiveLevel: number,
logger: Logger,
) => {
const mockInput: Record<string, any> = {};
if (recursiveLevel > 10) {
return mockInput;
}
const paramNames = Object.keys(inputParams || {});
for (let paramName of paramNames) {
if (['targetNamespace', 'targetNSAlias'].includes(paramName)) {
continue;
}
const paramValue = inputParams[paramName];
if (paramName.endsWith('[]')) {
paramName = paramName.slice(0, -2);
}
if (paramValue && typeof paramValue === 'object') {
mockInput[paramName] = mockProperties(endpointName, paramValue, recursiveLevel + 1, logger);
} else if (typeof paramValue === 'string') {
const paramType = paramValue.split(':')[1];
switch (paramType) {
case 'string':
mockInput[paramName] = 'Lorem ipsum';
break;
case 'date':
mockInput[paramName] = '2023-07-06';
break;
case 'decimal':
mockInput[paramName] = 42;
break;
default:
mockInput[paramName] = paramName;
}
} else {
const error = `Can't generate request example. Request param ${paramName} not supported in endpoint ${endpointName}`;
mockInput[paramName] = { error };
logger.warn(error);
continue;
}
}
return mockInput;
};
const slugFromEndpoint = (name: string): string => {
return name
.trim()
.replace(/[^\w\-]/g, '-')
.replaceAll(/-+/g, '-')
.toLowerCase()
.replace(/^-|-$/g, '');
};
function __dirname(url: string): string {
const __filename = fileURLToPath(url);
return path.dirname(__filename);
}
function fromCurrentDir(moduleUrl: string, path: string): string {
return resolve(__dirname(moduleUrl), path);
}
function getApiConfigByPath(
apis: RedoclyConfig['apis'],
relativePath: string,
): Record<string, any> {
for (const versionValue of Object.values(apis || {})) {
if (path.posix.normalize(versionValue.root) === relativePath) {
return {
...versionValue.theme,
...versionValue,
theme: undefined,
};
}
}
return {};
}