UNPKG

openapi-explorer

Version:

OpenAPI Explorer - API viewer with dynamically generated components, documentation, and interaction console

295 lines (285 loc) 13.4 kB
"use strict"; exports.__esModule = true; exports.default = ProcessSpec; var _openapiResolverBrowser = _interopRequireDefault(require("openapi-resolver/dist/openapi-resolver.browser.js")); var _marked = require("marked"); var _commonUtils = require("./common-utils.js"); var _lodash = _interopRequireDefault(require("lodash.clonedeep")); var _toposort = _interopRequireDefault(require("toposort")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // To inline test changes to the OpenAPiResolver, it must be copied into the node_modules directory. The builder for this package will not work across repositories. async function ProcessSpec(specUrlOrObject, serverUrl = '') { var _jsonParsedSpec$info, _jsonParsedSpec$compo; const inputSpecIsAUrl = typeof specUrlOrObject === 'string' && specUrlOrObject.match(/^http/) || typeof specUrlOrObject === 'object' && typeof specUrlOrObject.href === 'string'; let jsonParsedSpec; let errorToDisplay; for (let iteration = 0; iteration < 7; iteration++) { try { jsonParsedSpec = await (0, _openapiResolverBrowser.default)(specUrlOrObject); break; } catch (error) { // eslint-disable-next-line no-console console.error('Error parsing specification', error); errorToDisplay = error.message; await new Promise(resolve => setTimeout(resolve, 100 * 2 ** iteration)); } } if (!jsonParsedSpec) { if (errorToDisplay) { if (inputSpecIsAUrl && specUrlOrObject.toString().match('localhost')) { throw Error(`Cannot connect to your localhost running spec because your webserver is blocking requests. To the load the spec from ${specUrlOrObject.toString()}, return the following CORS header \`"Access-Control-Allow-Private-Network": "true"\`.`); } const message = `Failed to resolve the spec: ${errorToDisplay}`; throw Error(message); } throw Error('SpecificationNotFound'); } // Tags with Paths and WebHooks const tags = groupByTags(jsonParsedSpec); // Components const components = getComponents(jsonParsedSpec); // Info Description Headers const infoDescriptionHeaders = (_jsonParsedSpec$info = jsonParsedSpec.info) !== null && _jsonParsedSpec$info !== void 0 && _jsonParsedSpec$info.description ? getHeadersFromMarkdown(jsonParsedSpec.info.description) : []; // Security Scheme const securitySchemes = []; if ((_jsonParsedSpec$compo = jsonParsedSpec.components) !== null && _jsonParsedSpec$compo !== void 0 && _jsonParsedSpec$compo.securitySchemes) { Object.entries(jsonParsedSpec.components.securitySchemes).forEach(kv => { const securityObj = { apiKeyId: kv[0], ...kv[1] }; securityObj.value = ''; securityObj.finalKeyValue = ''; if (kv[1].type === 'apiKey' || kv[1].type === 'http') { securityObj.name = kv[1].name || 'Authorization'; securityObj.user = ''; securityObj.password = ''; } else if (kv[1].type === 'oauth2') { securityObj.name = 'Authorization'; securityObj.clientId = ''; securityObj.clientSecret = ''; } securitySchemes.push(securityObj); }); } // Servers let servers = []; if (Array.isArray(jsonParsedSpec.servers) && jsonParsedSpec.servers.length) { jsonParsedSpec.servers.filter(s => s).forEach(v => { let computedUrl = v.url.trim(); if (!(computedUrl.startsWith('http') || computedUrl.startsWith('//') || computedUrl.startsWith('{'))) { if (window.location.origin.startsWith('http')) { v.url = window.location.origin + v.url; computedUrl = v.url; } } // Apply server-variables to generate final computed-url if (v.variables) { Object.entries(v.variables).forEach(kv => { const regex = new RegExp(`{${kv[0]}}`, 'g'); computedUrl = computedUrl.replace(regex, kv[1].default || ''); kv[1].value = kv[1].default || ''; }); } v.computedUrl = computedUrl; }); const explicitServers = serverUrl && !jsonParsedSpec.servers.some(s => s.url === serverUrl || s.computedUrl === serverUrl) ? [{ url: serverUrl, computedUrl: serverUrl }] : []; servers = explicitServers.concat(jsonParsedSpec.servers); } else if (serverUrl) { servers = [{ url: serverUrl, computedUrl: serverUrl }]; } else if (inputSpecIsAUrl) { servers = [{ url: new URL(specUrlOrObject).origin, computedUrl: new URL(specUrlOrObject).origin }]; } else if (window.location.origin.startsWith('http')) { servers = [{ url: window.location.origin, computedUrl: window.location.origin }]; } else { servers = [{ url: 'http://localhost', computedUrl: 'http://localhost' }]; } const parsedSpec = { info: { ...jsonParsedSpec.info, expanded: true, headers: infoDescriptionHeaders }, tags, components, // pathGroups, externalDocs: jsonParsedSpec.externalDocs, securitySchemes, servers }; return parsedSpec; } function getHeadersFromMarkdown(markdownContent) { const tokens = _marked.marked.lexer(markdownContent); const headers = tokens.filter(v => v.type === 'heading' && v.depth <= 2); return headers || []; } function getComponents(openApiSpec) { if (!openApiSpec.components) { return []; } const components = []; for (const componentKeyId in openApiSpec.components) { const subComponents = Object.keys(openApiSpec.components[componentKeyId]).map(sComponentId => ({ expanded: true, id: `${componentKeyId.toLowerCase()}-${sComponentId.toLowerCase()}`.replace(_commonUtils.invalidCharsRegEx, '-'), name: openApiSpec.components[componentKeyId][sComponentId].title || sComponentId, component: openApiSpec.components[componentKeyId][sComponentId] })).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); if (componentKeyId === 'requestBodies' || componentKeyId === 'securitySchemes' || componentKeyId === 'securitySchemas') { continue; } components.push({ expanded: true, componentKeyId, subComponents }); } return components; } function groupByTags(openApiSpec) { const supportedMethods = ['get', 'query', 'put', 'post', 'patch', 'delete', 'head', 'options']; // this is also used for ordering endpoints by methods const rawTags = openApiSpec.tags && Array.isArray(openApiSpec.tags) ? openApiSpec.tags : []; const unsortedTags = rawTags // Only support nav tags in grouping, because these tags will be used for navigation. .filter(t => !t.kind || t.kind === 'nav').map(t => { const name = typeof t === 'string' ? t : t.name; return { elementId: `tag--${name.replace(_commonUtils.invalidCharsRegEx, '-')}`, name: name, summary: t.summary, description: t.description || '', headers: t.description ? getHeadersFromMarkdown(t.description) : [], paths: [], expanded: true }; }); const tagMap = unsortedTags.reduce((acc, t) => ({ ...acc, [t.name]: t }), {}); const tagDependencies = rawTags.map(t => t.parent && [t.parent, t.name] || [t.name, 'null']); const tags = (0, _toposort.default)(tagDependencies).map(tagName => tagMap[tagName]).filter(t => t); const pathsAndWebhooks = openApiSpec.paths || {}; if (openApiSpec.webhooks) { for (const [key, value] of Object.entries(openApiSpec.webhooks)) { value._type = 'webhook'; // eslint-disable-line no-underscore-dangle pathsAndWebhooks[key] = value; } } // For each path find the tag and push it into the corresponding tag for (const pathOrHookName in pathsAndWebhooks) { const commonPathPropServers = pathsAndWebhooks[pathOrHookName].servers || []; const isWebhook = pathsAndWebhooks[pathOrHookName]._type === 'webhook'; // eslint-disable-line no-underscore-dangle supportedMethods.forEach(methodName => { const commonParams = (0, _lodash.default)(pathsAndWebhooks[pathOrHookName].parameters || []); if (pathsAndWebhooks[pathOrHookName][methodName]) { const pathOrHookObj = openApiSpec.paths[pathOrHookName][methodName]; // If path.methods are tagged, else generate it from path const pathTags = Array.isArray(pathOrHookObj.tags) ? pathOrHookObj.tags : pathOrHookObj.tags && [pathOrHookObj.tags] || []; if (pathTags.length === 0) { pathTags.push('General ⦂'); } pathTags.forEach(tag => { var _pathOrHookObj$parame; let tagObj; let specTagsItem; if (openApiSpec.tags) { specTagsItem = tags.find(v => v.name.toLowerCase() === tag.toLowerCase()); } tagObj = tags.find(v => v.name === tag); if (!tagObj) { tagObj = { elementId: `tag--${tag.replace(_commonUtils.invalidCharsRegEx, '-')}`, name: tag, description: specTagsItem && specTagsItem.description || '', headers: specTagsItem && specTagsItem.description ? getHeadersFromMarkdown(specTagsItem.description) : [], paths: [], expanded: true }; tags.push(tagObj); } // Generate a short summary which is broken let shortSummary = (pathOrHookObj.summary || pathOrHookObj.description || `${methodName.toUpperCase()} ${pathOrHookName}`).trim(); if (shortSummary.length > 100) { shortSummary = shortSummary.split(/[.|!|?]\s|[\r?\n]/)[0]; // take the first line (period or carriage return) } // Merge Common Parameters with This methods parameters const finalParameters = ((_pathOrHookObj$parame = pathOrHookObj.parameters) === null || _pathOrHookObj$parame === void 0 ? void 0 : _pathOrHookObj$parame.slice(0)) || []; finalParameters.push(...commonParams.filter(commonParam => !finalParameters.some(param => commonParam.name === param.name && commonParam.in === param.in))); const successResponseKeys = Object.keys(pathOrHookObj.responses || {}).filter(r => !r.match(/^\d{3}$/i) || r.match(/^[23]\d{2}$/i)); const responseContentTypesMap = successResponseKeys.map(key => pathOrHookObj.responses[key]).reduce((acc, response) => Object.assign({}, acc, response.content || {}), {}); const responseContentTypes = Object.keys(responseContentTypesMap).sort((a, b) => a.localeCompare(b)); if (!finalParameters.some(p => p.in === 'header' && p.name.match(/^accept$/i)) && Object.keys(responseContentTypesMap).length > 1) { finalParameters.push({ in: 'header', name: 'Accept', description: 'Select the response body Content-Type. By default, the service will return a Content-Type that best matches the requested type.', schema: { type: 'string', enum: responseContentTypes }, default: responseContentTypes[0], example: responseContentTypes[0] }); } // Remove bad callbacks if (pathOrHookObj.callbacks) { for (const [callbackName, callbackConfig] of Object.entries(pathOrHookObj.callbacks)) { const originalCallbackEntries = Object.entries(callbackConfig); const filteredCallbacks = originalCallbackEntries.filter(entry => typeof entry[1] === 'object') || []; pathOrHookObj.callbacks[callbackName] = Object.fromEntries(filteredCallbacks); if (filteredCallbacks.length !== originalCallbackEntries.length) { console.warn(`OpenAPI Explorer: Invalid Callback found in ${callbackName}`); // eslint-disable-line no-console } } } // Update Responses const pathObject = { expanded: false, isWebhook, summary: pathOrHookObj.summary || '', description: pathOrHookObj.description || '', shortSummary, method: methodName, path: pathOrHookName, operationId: pathOrHookObj.operationId, elementId: `${methodName}-${pathOrHookName.replace(_commonUtils.invalidCharsRegEx, '-')}`, servers: pathOrHookObj.servers ? commonPathPropServers.concat(pathOrHookObj.servers) : commonPathPropServers, parameters: finalParameters, requestBody: pathOrHookObj.requestBody, responses: pathOrHookObj.responses, callbacks: pathOrHookObj.callbacks, deprecated: pathOrHookObj.deprecated, security: pathOrHookObj.security || openApiSpec.security, externalDocs: pathOrHookObj.externalDocs, // commonSummary: commonPathProp.summary, // commonDescription: commonPathProp.description, xCodeSamples: pathOrHookObj['x-code-samples'] || '', extensions: Object.keys(pathOrHookObj).filter(k => k.startsWith('x-') && k !== 'x-code-samples').reduce((acc, extensionKey) => { acc[extensionKey] = pathOrHookObj[extensionKey]; return acc; }, {}) }; tagObj.paths.push(pathObject); }); // End of tag path create } }); // End of Methods } return tags; }