swagger-typescript
Version:
An auto ts/js code generate from swagger/openApi
599 lines • 24 kB
JavaScript
/* eslint-disable prefer-const */
;
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