UNPKG

@redocly/portal-plugin-soap-api

Version:

SOAP API plugin for Redocly products

275 lines (240 loc) 7.83 kB
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 {}; }