UNPKG

swagger-typescript

Version:

An auto ts/js code generate from swagger/openApi

599 lines 24 kB
/* eslint-disable prefer-const */ "use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { promises } from "fs"; import { dump } from "js-yaml"; import { parseMdTable } from "./md-utils"; import replacePostmanVariables from "./var-replacer"; import jsonc from "jsonc-parser"; import camelCase from "lodash.camelcase"; const { writeFile, readFile } = promises; async function postmanToOpenApi(input, output, { info = {}, defaultTag = "default", pathDepth = 0, auth: optsAuth, servers, externalDocs = {}, folders = {}, responseHeaders = true, replaceVars = false, additionalVars = {}, outputFormat = "yaml", disabledParams = { includeQuery: false, includeHeader: false }, operationId = "off", } = {}) { // TODO validate? let collectionFile = await resolveInput(input); if (replaceVars) { collectionFile = replacePostmanVariables(collectionFile, additionalVars); } const _postmanJson = JSON.parse(collectionFile); const postmanJson = _postmanJson.collection || _postmanJson; const { item: items, variable = [] } = postmanJson; const paths = {}; const domains = new Set(); const tags = {}; const securitySchemes = {}; for (let [i, element] of items.entries()) { while (element != null && element.item != null) { // is a folder const { item, description: tagDesc } = element; const tag = calculateFolderTag(element, folders); const tagged = item.map((e) => (Object.assign(Object.assign({}, e), { tag }))); tags[tag] = tagDesc; items.splice(i, 1, ...tagged); // Empty folders will have tagged empty element = tagged.length > 0 ? tagged.shift() : items[i]; } // If there are an empty folder at the end of the collection elements could be `undefined` if (element != null) { const { request: { url, method, body, description: rawDesc, header = [], auth }, name, tag = defaultTag, event: events, response, } = element; const { path, query, protocol, host, port, valid, pathVars } = scrapeURL(url); if (valid) { // Remove from name the possible operation id between brackets // eslint-disable-next-line no-useless-escape const summary = name.replace(/ \[([^\[\]]*)\]/gi, ""); domains.add(calculateDomains(protocol, host, port)); const joinedPath = calculatePath(path, pathDepth); if (!paths[joinedPath]) paths[joinedPath] = {}; const { description, paramsMeta } = descriptionParse(rawDesc); paths[joinedPath][method.toLowerCase()] = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ tags: [tag], summary }, calculateOperationId(operationId, name, summary)), (description ? { description } : {})), parseBody(body, method)), parseOperationAuth(auth, securitySchemes, optsAuth)), parseParameters(query, header, joinedPath, paramsMeta, pathVars, disabledParams)), parseResponse(response, events, responseHeaders)); } } } const openApi = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ openapi: "3.0.0", info: compileInfo(postmanJson, info) }, parseExternalDocs(variable, externalDocs)), parseServers(domains, servers)), parseAuth(postmanJson, optsAuth, securitySchemes)), parseTags(tags)), { paths }); const openApiDoc = outputFormat === "json" ? JSON.stringify(openApi, null, 4) : dump(openApi, { skipInvalid: true }); if (output != null) { await writeFile(output, openApiDoc, "utf8"); } return openApiDoc; } /* Calculate the tags for folders items based on the options */ function calculateFolderTag({ tag, name }, { separator = " > ", concat = true }) { return tag && concat ? `${tag}${separator}${name}` : name; } function compileInfo(postmanJson, optsInfo) { const { info: { name, description: desc }, variable = [], } = postmanJson; const ver = getVarValue(variable, "version", "1.0.0"); const { title = name, description = desc, version = ver, termsOfService, license, contact, xLogo, } = optsInfo; return Object.assign(Object.assign(Object.assign(Object.assign({ title, description, version }, parseXLogo(variable, xLogo)), (termsOfService ? { termsOfService } : {})), parseContact(variable, contact)), parseLicense(variable, license)); } function parseXLogo(variables, xLogo = {}) { const urlVar = getVarValue(variables, "x-logo.urlVar"); const backgroundColorVar = getVarValue(variables, "x-logo.backgroundColorVar"); const altTextVar = getVarValue(variables, "x-logo.altTextVar"); const hrefVar = getVarValue(variables, "x-logo.hrefVar"); const { url = urlVar, backgroundColor = backgroundColorVar, altText = altTextVar, href = hrefVar, } = xLogo; return url != null ? { "x-logo": { url, backgroundColor, altText, href } } : {}; } function parseLicense(variables, optsLicense = {}) { const nameVar = getVarValue(variables, "license.name"); const urlVar = getVarValue(variables, "license.url"); const { name = nameVar, url = urlVar } = optsLicense; return name != null ? { license: Object.assign({ name }, (url ? { url } : {})) } : {}; } function parseContact(variables, optsContact = {}) { const nameVar = getVarValue(variables, "contact.name"); const urlVar = getVarValue(variables, "contact.url"); const emailVar = getVarValue(variables, "contact.email"); const { name = nameVar, url = urlVar, email = emailVar } = optsContact; return [name, url, email].some((e) => e != null) ? { contact: Object.assign(Object.assign(Object.assign({}, (name ? { name } : {})), (url ? { url } : {})), (email ? { email } : {})), } : {}; } function parseExternalDocs(variables, optsExternalDocs) { const descriptionVar = getVarValue(variables, "externalDocs.description"); const urlVar = getVarValue(variables, "externalDocs.url"); const { description = descriptionVar, url = urlVar } = optsExternalDocs; return url != null ? { externalDocs: Object.assign({ url }, (description ? { description } : {})) } : {}; } function parseBody(body = {}, method) { // Swagger validation return an error if GET has body if (["GET", "DELETE"].includes(method)) return {}; const { mode, raw, options = { raw } } = body; let content = {}; switch (mode) { case "raw": { const { raw: { language }, } = options; let example = ""; if (language === "json") { if (raw) { const errors = []; example = jsonc.parse(raw, errors); if (errors.length > 0) { example = raw; } } content = { "application/json": { schema: { type: "object", example, }, }, }; } else if (language === "text") { content = { "text/plain": { schema: { type: "string", example: raw, }, }, }; } else { content = { "*/*": { schema: { type: "string", // To protect from object types we always stringify this example: JSON.stringify(raw), }, }, }; } break; } case "file": content = { "text/plain": {}, }; break; case "formdata": { content = { "multipart/form-data": parseFormData(body.formdata), }; break; } case "urlencoded": content = { "application/x-www-form-urlencoded": parseFormData(body.urlencoded), }; break; } return { requestBody: { content } }; } /** Parse the body for create a form data structure */ function parseFormData(data) { const objectSchema = { schema: { type: "object", }, }; return data.reduce((obj, { key, type, description, value }) => { const { schema } = obj; if (isRequired(description)) { (schema.required = schema.required || []).push(key); } (schema.properties = schema.properties || {})[key] = Object.assign(Object.assign(Object.assign({ type: inferType(value) }, (description ? { description: description.replace(/ ?\[required\] ?/gi, "") } : {})), (value ? { example: value } : {})), (type === "file" ? { format: "binary" } : {})); return obj; }, objectSchema); } /** * Default logic to insert parameters, if parameter exist will not be inserted * again. In Postman this means that only the first parameter is used, the * repeated ones are discarded. This is a separated method to allow make it * configurable in the future * * @param {Map} parameterMap * @param {Object} param * @returns The modified parameterMap */ const defaultParamInserter = (parameterMap, param) => { if (!parameterMap.has(param.name)) { parameterMap.set(param.name, param); } return parameterMap; }; /* Parse the Postman query and header and transform into OpenApi parameters */ function parseParameters(query, header, paths, paramsMeta = {}, pathVars, { includeQuery = false, includeHeader = false, }, paramInserter = defaultParamInserter) { // parse Headers const parameters = [ ...header .reduce(mapParameters("header", includeHeader, paramInserter), new Map()) .values(), ]; // parse Query parameters.push(...query .reduce(mapParameters("query", includeQuery, paramInserter), new Map()) .values()); // Path params parameters.push(...extractPathParameters(paths, paramsMeta, pathVars)); return parameters.length ? { parameters } : {}; } /* Accumulator function for different types of parameters */ function mapParameters(type, includeDisabled, paramInserter) { return (parameterMap, { key, description, value, disabled }) => { if (!includeDisabled && disabled === true) return parameterMap; const required = /\[required\]/gi.test(description); paramInserter(parameterMap, Object.assign(Object.assign(Object.assign({ name: key, in: type, schema: { type: inferType(value) } }, (required ? { required } : {})), (description ? { description: description.replace(/ ?\[required\] ?/gi, "") } : {})), (value ? { example: value } : {}))); return parameterMap; }; } function extractPathParameters(path, paramsMeta, pathVars) { const matched = path.match(/{\s*[\w-]+\s*}/g) || []; return matched.map((match) => { const name = match.slice(1, -1); const { type: varType = "string", description: desc, value, } = pathVars[name] || {}; const { type = varType, description = desc, example = value, } = paramsMeta[name] || {}; return Object.assign(Object.assign({ name, in: "path", schema: { type }, required: true }, (description ? { description } : {})), (example ? { example } : {})); }); } function getVarValue(variables, name, def = undefined) { const variable = variables.find(({ key }) => key === name); return variable ? variable.value : def; } /* calculate the type of a variable based on OPenApi types */ function inferType(value) { if (/^\d+$/.test(value)) return "integer"; if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(value)) return "number"; if (/^(true|false)$/.test(value)) return "boolean"; return "string"; } /* Calculate the global auth based on options and postman definition */ function parseAuth({ auth }, optAuth, securitySchemes) { if (optAuth != null) { return parseOptsAuth(optAuth); } return parsePostmanAuth(auth, securitySchemes); } /* Parse a postman auth definition */ function parsePostmanAuth(postmanAuth = {}, securitySchemes) { const { type } = postmanAuth; if (type != null) { securitySchemes[`${type}Auth`] = { type: "http", scheme: type, }; return { components: { securitySchemes }, security: [ { [`${type}Auth`]: [], }, ], }; } return Object.keys(securitySchemes).length === 0 ? {} : { components: { securitySchemes } }; } /* Parse Auth at operation/request level */ function parseOperationAuth(auth, securitySchemes, optsAuth) { if (auth == null || optsAuth != null) { // In case of config auth operation auth is disabled return {}; } else { const { type } = auth; securitySchemes[`${type}Auth`] = { type: "http", scheme: type, }; return { security: [{ [`${type}Auth`]: [] }], }; } } /* Parse a options global auth */ function parseOptsAuth(optAuth) { const securitySchemes = {}; const security = []; for (const [secName, secDefinition] of Object.entries(optAuth)) { const { type, scheme } = secDefinition, rest = __rest(secDefinition, ["type", "scheme"]); if (type === "http" && ["bearer", "basic"].includes(scheme)) { securitySchemes[secName] = Object.assign({ type: "http", scheme }, rest); security.push({ [secName]: [] }); } } return Object.keys(securitySchemes).length === 0 ? {} : { components: { securitySchemes }, security, }; } /* From the path array compose the real path for OpenApi specs */ function calculatePath(paths, pathDepth) { paths = paths.slice(pathDepth); // path depth // replace repeated '{' and '}' chars // replace `:` chars at first return ("/" + paths .map((path) => { path = path.replace(/([{}])\1+/g, "$1"); path = path.replace(/^:(.*)/g, "{$1}"); return path; }) .join("/")); } function calculateDomains(protocol, hosts, port) { return protocol + "://" + hosts.join(".") + (port ? `:${port}` : ""); } /** * To support postman collection v2 and variable replace we should parse the * `url` or `url.raw` data without trust in the object as in v2 could not exist * and if replaceVars = true then values cannot be correctly parsed * * @param {Object | String} url * @returns A url structure as in postman v2.1 collections */ function scrapeURL(url) { // Avoid parse empty url request if (url === undefined || url === "" || url.raw === "") { return { valid: false }; } const rawUrl = typeof url === "string" || url instanceof String ? url : url.raw; // Fix for issue #136 if replace vars are not used then new URL throw an error // when using variables before the schema const fixedUrl = rawUrl.startsWith("{{") ? "http://" + rawUrl : rawUrl; //@ts-ignore const objUrl = new URL(fixedUrl); return { raw: rawUrl, path: decodeURIComponent(objUrl.pathname).slice(1).split("/"), query: compoundQueryParams(objUrl.searchParams, url.query), protocol: objUrl.protocol.slice(0, -1), host: decodeURIComponent(objUrl.hostname).split("."), port: objUrl.port, valid: true, pathVars: url.variable == null ? {} : url.variable.reduce((obj, { key, value, description }) => { obj[key] = { value, description, type: inferType(value) }; return obj; }, {}), }; } /** * Calculate query parameters in postman collection * * @param {any} searchParams The searchParam instance from an URL object * @param {any} queryCollection The postman collection query section * @returns A query params array as created by postman collections Array(Obj) * * NOTE: This method was created because we think that some versions of postman * don´t add the `query` parameter in the url, but after some reasearch the * reason why the `query` parameter can not be present is just because no * query parameters are used so we just format the postman `query` array * here. */ function compoundQueryParams(searchParams, queryCollection = []) { return queryCollection; } /* Parse domains from operations or options */ function parseServers(domains, serversOpts) { let servers; if (serversOpts != null) { // This map is just to filter not supported fields while no validations are implemented servers = serversOpts.map(({ url, description }) => ({ url, description })); } else { servers = Array.from(domains).map((domain) => ({ url: domain })); } return servers.length > 0 ? { servers } : {}; } /* Transform a object of tags in an array of tags */ function parseTags(tagsObj) { const tags = Object.entries(tagsObj).map(([name, description]) => ({ name, description, })); return tags.length > 0 ? { tags } : {}; } function descriptionParse(description) { if (description == null) return { description }; const splitDesc = description.split(/# postman-to-openapi/gi); if (splitDesc.length === 1) return { description }; return { description: splitDesc[0].trim(), paramsMeta: parseMdTable(splitDesc[1]), }; } function parseResponse(responses, events, responseHeaders) { if (responses != null && Array.isArray(responses) && responses.length > 0) { return parseResponseFromExamples(responses, responseHeaders); } else { return { responses: parseResponseFromEvents(events) }; } } function parseResponseFromEvents(events = []) { let status = 200; const test = events.filter((event) => event.listen === "test"); if (test.length > 0) { const script = test[0].script.exec.join(); const result = script.match(/\.response\.code\)\.to\.eql\((\d{3})\)|\.to\.have\.status\((\d{3})\)/); status = result && result[1] != null ? result[1] : result && result[2] != null ? result[2] : status; } return { [status]: { description: "Successful response", content: { "application/json": {}, }, }, }; } function parseResponseFromExamples(responses, responseHeaders) { // Group responses by status code const statusCodeMap = responses.reduce((statusMap, { name, code, status: description, header, body, _postman_previewlanguage: language, }) => { if (code in statusMap) { if (!(language in statusMap[code].bodies)) { statusMap[code].bodies[language] = []; } statusMap[code].bodies[language].push({ name, body }); } else { statusMap[code] = { description, header, bodies: { [language]: [{ name, body }] }, }; } return statusMap; }, {}); // Parse for OpenAPI const parsedResponses = Object.entries(statusCodeMap).reduce((parsed, [status, { description, header, bodies }]) => { parsed[status] = Object.assign(Object.assign({ description }, parseResponseHeaders(header, responseHeaders)), parseContent(bodies)); return parsed; }, {}); return { responses: parsedResponses }; } function parseContent(bodiesByLanguage) { const content = Object.entries(bodiesByLanguage).reduce((content, [language, bodies]) => { if (language === "json") { content["application/json"] = Object.assign({ schema: { type: "object" } }, parseExamples(bodies, "json")); } else { content["text/plain"] = Object.assign({ schema: { type: "string" } }, parseExamples(bodies, "text")); } return content; }, {}); return { content }; } function parseExamples(bodies, language) { if (Array.isArray(bodies) && bodies.length > 1) { return { examples: bodies.reduce((ex, { name: summary, body }, i) => { ex[`example-${i}`] = { summary, value: safeSampleParse(body, summary, language), }; return ex; }, {}), }; } else { const { body, name } = bodies[0]; return { example: safeSampleParse(body, name, language), }; } } function safeSampleParse(body, name, language) { if (language === "json") { const errors = []; const parsedBody = jsonc.parse(body == null || body.trim().length === 0 ? "{}" : body, errors); if (errors.length > 0) { throw new Error('Error parsing response example "' + name + '"'); } return parsedBody; } return body; } function parseResponseHeaders(headerArray, responseHeaders) { if (!responseHeaders) { return {}; } headerArray = headerArray || []; const headers = headerArray.reduce((acc, { key, value }) => { acc[key] = { schema: { type: inferType(value), example: value, }, }; return acc; }, {}); return Object.keys(headers).length > 0 ? { headers } : {}; } /** * Just check if is a string collection or a path. moved to method for allow * easy changes in the future like check if it is a collection, validations... */ async function resolveInput(input) { if (input.trim().startsWith("{")) { return input; } else { return readFile(input, "utf8"); } } /** * Return if the provided text contains the '[required]' mark * * @param {any} text The text where we should look for the required mark * @returns Boolean */ function isRequired(text) { return /\[required\]/gi.test(text); } /** * Calculate the operationId based on the user selected `mode` * * @param {any} mode - Mode to calculate the operation id between `off`, `auto` * or `brackets` * @param {any} name - Field name of the request/operation in the postman * collection without modify. * @param {any} summary - Calculated summary of the operation that will be used * in the OpenAPI spec. * @returns An operation id */ function calculateOperationId(mode, name, summary) { let operationId; switch (mode) { case "off": break; case "auto": operationId = camelCase(summary); break; case "brackets": { // eslint-disable-next-line no-useless-escape const matches = name.match(/\[([^\[\]]*)\]/); operationId = matches ? matches[1] : undefined; break; } default: // Unknown value in the operationId option break; } return operationId ? { operationId } : {}; } export default postmanToOpenApi; //# sourceMappingURL=index.js.map