avantation
Version:
Build OpenAPI3.0 specification from HAR
387 lines (386 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AvantationAPI = void 0;
const tslib_1 = require("tslib");
const OASEnum = tslib_1.__importStar(require("../enums/oas"));
const querystring = tslib_1.__importStar(require("querystring"));
const util_1 = require("./util");
const fs = tslib_1.__importStar(require("fs"));
const path = tslib_1.__importStar(require("path"));
const logger_1 = require("./logger");
const HTTPSnippet = require('httpsnippet');
var URL = require('url-parse');
const YAML = require('json2yaml');
class AvantationAPI {
constructor(input, oclif) {
this.tagsHolder = {};
this.har = input.har;
this.title = input.title;
this.host = input.host;
this.basePath = input.basePath;
this.pathParamRegex = input.pathParamRegex;
this.pipe = input.pipe;
this.json = input.json;
this.template = input.template;
this.out = input.out;
this.pathRegex = new RegExp(this.pathParamRegex);
this.oclif = oclif;
this.mimeTypes = ['application/json', '', 'application/json; charset=utf-8'];
this.disableTag = input.disableTag;
this.securityHeaders = input.securityHeaders;
this['http-snippet'] = input['http-snippet'];
this.run();
}
async run() {
this.har.log.entries.forEach(this.buildEntry.bind(this));
this.onBuildComplete();
}
logSuccess(head) {
this.oclif.log(`${logger_1.colors.Bright}${logger_1.colors.fg.Green}✔ ${logger_1.colors.Reset}${head}`);
}
buildEntry(entry) {
let url = new URL(entry.request.url);
let method;
entry.response.content.mimeType =
entry.response.content.mimeType === 'application/json; charset=utf-8'
? 'application/json'
: entry.response.content.mimeType;
if (!this.mimeTypes.includes(entry.response.content.mimeType)) {
this.oclif.warn(`Skipping invalid mimeType:${entry.response.content.mimeType} @${url.href} in response.`);
return;
}
if (url.host !== this.host || !url.pathname.includes(this.basePath)) {
this.oclif.warn(`Skipping invalid url ${url.href}`);
return; //simply ignore invalid url match
}
// console.log(`query:${url.query} and path:${url.pathname}`);
let path = this.buildPathDetails(url);
if (path === undefined)
return; //simply ignore invalid path match api
let hardCodedQuery = this.buildHardCodedQueryParams(url);
let queryParams = this.buildQueryParams(entry.request.queryString);
// Avoid duplicating hardcoded query params from the URL, and those
// specified in the HAR request queryString
let uniqueQueryParams = new Map();
hardCodedQuery.forEach((q) => {
uniqueQueryParams.set(q.name, q);
});
queryParams.forEach((q) => {
if (!uniqueQueryParams.has(q.name)) {
uniqueQueryParams.set(q.name, q);
}
});
let requestBody = this.buildRequestBody(entry.request.postData, url);
let response = this.buildResponse(entry.response);
let security = this.buildSecurity(entry.request.headers);
let pathItemInfo = this.buildTag(entry.comment, path.tag);
let operationItem = {
security: Object.keys(security).length > 0 ? [security] : [],
tags: [pathItemInfo.tag],
summary: pathItemInfo.comment || pathItemInfo.tag,
parameters: [...path.params, ...uniqueQueryParams.values()],
requestBody: requestBody,
responses: response
};
if (this['http-snippet']) {
operationItem['x-code-samples'] = this.generateSampleCodes(entry.request);
}
if (this.disableTag)
delete operationItem.tags;
if (!this.template.paths[path.value])
this.template.paths[path.value] = {};
this.tagsHolder[pathItemInfo.tag] = pathItemInfo.tag;
switch (entry.request.method.toLocaleLowerCase()) {
case 'post':
method = `${logger_1.colors.Bright}${logger_1.colors.fg.Green}POST ${logger_1.colors.Reset}`;
this.template.paths[path.value].post = operationItem;
break;
case 'get':
method = `${logger_1.colors.Bright}${logger_1.colors.fg.Blue}GET ${logger_1.colors.Reset}`;
this.template.paths[path.value].get = operationItem;
break;
case 'put':
method = `${logger_1.colors.Bright}${logger_1.colors.fg.Magenta}PUT ${logger_1.colors.Reset}`;
this.template.paths[path.value].put = operationItem;
break;
case 'delete':
method = `${logger_1.colors.Bright}${logger_1.colors.fg.Red}DEL ${logger_1.colors.Reset}`;
this.template.paths[path.value].delete = operationItem;
break;
case 'del':
method = `${logger_1.colors.Bright}${logger_1.colors.fg.Red}DEL ${logger_1.colors.Reset}`;
this.template.paths[path.value].delete = operationItem;
break;
}
if (!this.pipe)
this.logSuccess(method + '\t' + url.pathname);
}
buildPathDetails(url) {
let basePathArr = this.basePath === '' ? ['', url.pathname] : url.pathname.split(this.basePath);
if (basePathArr.length !== 2) {
this.oclif.warn('Skipping following invalid path API:' +
JSON.stringify({
host: url.host,
path: url.pathname,
basePath: this.basePath
}, null, 4));
return undefined;
}
let pathArr = basePathArr[1].split('/');
let pathTag = undefined;
let that = this;
let dynamicPathParam = [];
let dynamicPathProcessId = 0;
pathArr.forEach(function (path, index) {
if (!pathTag)
pathTag = path;
let isDynamicPath = that.pathRegex.test(path);
if (isDynamicPath) {
let name = 'id' + (dynamicPathProcessId > 0 ? dynamicPathProcessId : '');
dynamicPathProcessId++;
let gPath = {
in: OASEnum.ParameterObject.IN.Path,
name: name,
schema: {
type: 'string'
},
required: true
};
dynamicPathParam.push(gPath);
pathArr[index] = '{' + name + '}';
}
});
return {
params: dynamicPathParam,
value: pathArr.join('/'),
tag: pathTag
};
}
buildHardCodedQueryParams(url) {
let pathItemObject = [];
if (!url.query)
return pathItemObject;
let queryStr = url.query.split('?')[1];
if (!queryStr)
return pathItemObject;
let query = querystring.parse(queryStr);
for (let prop in query) {
let item;
item = {
in: OASEnum.ParameterObject.IN.Query,
name: prop,
schema: {
type: 'string'
},
required: true
};
pathItemObject.push(item);
}
return pathItemObject;
}
buildQueryParams(queryArray) {
let params = [];
params = queryArray.map(function (query) {
let param = {
in: OASEnum.ParameterObject.IN.Query,
name: query.name,
schema: {
type: 'string'
},
required: true
};
return param;
});
return params;
}
buildRequestBody(postData, url) {
if (postData == undefined || !postData.mimeType)
return undefined;
let param = {
required: true,
content: {}
};
if (postData.mimeType) {
switch (postData.mimeType.split(';')[0].toLocaleLowerCase()) {
case 'application/json':
case 'application/json; charset=utf-8':
let data = postData.text ? JSON.parse(postData.text) : {};
param.content[postData.mimeType] = {
schema: util_1.Util.generateSchema(data),
example: data
};
break;
case 'multipart/form-data':
param.content['multipart/form-data'] = {
schema: this.buildFormData(postData)
};
break;
case 'application/x-www-form-urlencoded':
param.content['application/x-www-form-urlencoded'] = {
schema: this.buildFormData(postData)
};
break;
default:
this.oclif.warn(`currently mimeType:${postData.mimeType} not supported. ${url.href}`);
}
}
return param;
}
buildFormData(postData) {
if (postData.params !== undefined && postData.params.length !== 0) {
let properties = {};
let required = postData.params.map(function (query) {
if (query.value == '' || query.value == '(binary)') {
properties[query.name] = {
type: 'string',
format: 'binary'
};
return query.name;
}
properties[query.name] = {
type: 'string'
};
return query.name;
});
return {
type: 'object',
properties: properties,
required: required
};
}
return util_1.Util.generateSchema({});
}
buildResponse(res) {
let response = {
default: {
description: 'Unexpected error',
content: {
['application/json']: {
example: {
message: 'Sorry unable to perform operation.'
}
}
}
}
};
if (!res.content.text || !res.content.mimeType.includes('application/json'))
return response;
if (res.content.encoding && res.content.encoding == 'base64') {
res.content.text = Buffer.from(res.content.text, 'base64').toString();
}
let responseData = JSON.parse(res.content.text);
if (responseData instanceof Array) {
responseData = responseData.slice(0, 3);
}
else {
util_1.Util.arrayMaxDepth(responseData, 3);
}
let responObject = {
description: res.statusText,
content: {
[res.content.mimeType]: {
schema: util_1.Util.generateSchema(responseData),
example: responseData
}
}
};
response[res.status] = responObject;
return response;
}
buildSecurity(headers) {
let security = {};
let that = this;
headers.forEach(function (header) {
if (header.name.trim().toLocaleLowerCase() === 'authorization')
security['JWT'] = [];
if (that.securityHeaders[header.name.trim()])
security[header.name.trim()] = [];
});
return security;
}
buildTag(comment, pathTag) {
if (!comment || !comment.includes('#'))
return {
tag: pathTag || ''
};
let commentArr = comment.split('#');
let data = querystring.parse(commentArr[1]);
return {
tag: data.tag || pathTag || '',
comment: commentArr[0]
};
}
generateSampleCodes(harRequest) {
let snip = new HTTPSnippet(harRequest);
return [
{
lang: 'Curl',
source: snip.convert('shell', 'curl')
},
{
lang: 'JavaScript',
source: snip.convert('javascript', 'jquery')
},
{
lang: 'OKHttp',
source: snip.convert('java', 'okhttp')
},
{
lang: 'Swift',
source: snip.convert('swift')
},
{
lang: 'Python',
source: snip.convert('python', 'requests')
},
{
lang: 'NodeJS',
source: snip.convert('node', 'native')
}
];
}
onBuildComplete() {
if (!this.template.tags)
this.template.tags = [];
if (!this.disableTag)
for (let tag in this.tagsHolder) {
this.template.tags.push({
name: tag
});
}
let that = this;
this.template.info.title = that.title;
this.template.servers.forEach(function (server) {
server.url = server.url.replace('{host}', that.host);
if (server.variables && server.variables.basePath && typeof server.variables.basePath == 'object') {
server.variables.basePath.default = server.variables.basePath.default.replace('{basePath}', that.basePath);
}
});
if (this.template.components && this.template.components.securitySchemes) {
for (let security in this.securityHeaders) {
this.template.components.securitySchemes[security] = this.securityHeaders[security];
}
}
if (this.pipe) {
console.log(this.json ? JSON.stringify(this.template) : YAML.stringify(this.template));
return;
}
if (this.json || path.extname(this.out) == '.json') {
if (this.out.endsWith('.yaml')) {
this.out = this.out.replace('.yaml', '.json');
}
let _path = this.out ? path.resolve(this.out) : path.join(process.cwd(), 'openapi.json');
fs.writeFileSync(_path, JSON.stringify(this.template, null, 4));
this.afterBuildComplete();
return;
}
fs.writeFileSync(this.out ? path.resolve(this.out) : path.join(process.cwd(), 'openapi.yaml'), YAML.stringify(this.template));
this.afterBuildComplete();
return;
}
afterBuildComplete() {
this.logSuccess('all taskes completed');
}
buildStaticUI() { }
}
exports.AvantationAPI = AvantationAPI;