Merge in DNS/adguard-home from beta-client-2 to master
Squashed commit of the following:
commit b2640cc49a6c5484d730b534dcf5a8013d7fa478
Merge: 659def862 aef4659e9
Author: Eugene Burkov <e.burkov@adguard.com>
Date: Tue Dec 29 19:23:09 2020 +0300
Merge branch 'master' into beta-client-2
commit 659def8626467949c35b7a6a0c99ffafb07b4385
Author: Eugene Burkov <e.burkov@adguard.com>
Date: Tue Dec 29 17:25:14 2020 +0300
all: upgrade github actions node version
commit b4b8cf8dd75672e9155da5d111ac66e8f5ba1535
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date: Tue Dec 29 16:57:14 2020 +0300
all: beta client squashed
318 lines
12 KiB
TypeScript
318 lines
12 KiB
TypeScript
/* eslint-disable no-template-curly-in-string */
|
|
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { stringify } from 'qs';
|
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
import * as morph from 'ts-morph';
|
|
|
|
import {
|
|
API_DIR as API_DIR_CONST,
|
|
GENERATOR_ENTITY_ALLIAS,
|
|
} from '../../consts';
|
|
import { toCamel, capitalize, schemaParamParser } from './utils';
|
|
|
|
|
|
const API_DIR = path.resolve(API_DIR_CONST);
|
|
if (!fs.existsSync(API_DIR)) {
|
|
fs.mkdirSync(API_DIR);
|
|
}
|
|
|
|
const { Project, QuoteKind } = morph;
|
|
|
|
|
|
class ApiGenerator {
|
|
project = new Project({
|
|
tsConfigFilePath: './tsconfig.json',
|
|
addFilesFromTsConfig: false,
|
|
manipulationSettings: {
|
|
quoteKind: QuoteKind.Single,
|
|
usePrefixAndSuffixTextForRename: false,
|
|
useTrailingCommas: true,
|
|
},
|
|
});
|
|
|
|
openapi: Record<string, any>;
|
|
|
|
serverUrl: string;
|
|
|
|
paths: any;
|
|
|
|
/* interface Controllers {
|
|
[controller: string]: {
|
|
[operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
|
|
}
|
|
} */
|
|
controllers: Record<string, any> = {};
|
|
|
|
apis: morph.SourceFile[] = [];
|
|
|
|
constructor(openapi: Record<string, any>) {
|
|
this.openapi = openapi;
|
|
this.paths = openapi.paths;
|
|
this.serverUrl = openapi.servers[0].url;
|
|
|
|
Object.keys(this.paths).forEach((pathKey) => {
|
|
Object.keys(this.paths[pathKey]).forEach((method) => {
|
|
const {
|
|
tags, operationId, parameters, responses, requestBody, security,
|
|
} = this.paths[pathKey][method];
|
|
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
|
|
|
|
if (this.controllers[controller]) {
|
|
this.controllers[controller][operationId] = {
|
|
parameters,
|
|
responses,
|
|
method,
|
|
requestBody,
|
|
security,
|
|
pathKey: pathKey.replace(/{/g, '${'),
|
|
};
|
|
} else {
|
|
this.controllers[controller] = { [operationId]: {
|
|
parameters,
|
|
responses,
|
|
method,
|
|
requestBody,
|
|
security,
|
|
pathKey: pathKey.replace(/{/g, '${'),
|
|
} };
|
|
}
|
|
});
|
|
});
|
|
|
|
this.generateApiFiles();
|
|
}
|
|
|
|
generateApiFiles = () => {
|
|
Object.keys(this.controllers).forEach(this.generateApiFile);
|
|
};
|
|
|
|
generateApiFile = (cName: string) => {
|
|
const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
|
|
apiFile.addStatements([
|
|
'// This file was autogenerated. Please do not change.',
|
|
'// All changes will be overwrited on commit.',
|
|
'',
|
|
]);
|
|
|
|
// const schemaProperties = schemas[schemaName].properties;
|
|
const importEntities: any[] = [];
|
|
|
|
// add api class to file
|
|
const apiClass = apiFile.addClass({
|
|
name: `${capitalize(cName)}Api`,
|
|
isDefaultExport: true,
|
|
});
|
|
|
|
// get operations of controller
|
|
const controllerOperations = this.controllers[cName];
|
|
const operationList = Object.keys(controllerOperations).sort();
|
|
// for each operation add fetcher
|
|
operationList.forEach((operation) => {
|
|
const {
|
|
requestBody, responses, parameters, method, pathKey, security,
|
|
} = controllerOperations[operation];
|
|
|
|
const queryParams: any[] = []; // { name, type }
|
|
const bodyParam: any[] = []; // { name, type }
|
|
|
|
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
|
|
let contentType = '';
|
|
if (parameters) {
|
|
parameters.forEach((p: any) => {
|
|
const [
|
|
pType, isArray, isClass, isImport,
|
|
] = schemaParamParser(p.schema, this.openapi);
|
|
|
|
if (isImport) {
|
|
importEntities.push({ type: pType, isClass });
|
|
}
|
|
if (p.in === 'query') {
|
|
queryParams.push({
|
|
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
|
|
}
|
|
});
|
|
}
|
|
if (queryParams.length > 0) {
|
|
const imp = apiFile.getImportDeclaration((i) => {
|
|
return i.getModuleSpecifierValue() === 'qs';
|
|
}); if (!imp) {
|
|
apiFile.addImportDeclaration({
|
|
moduleSpecifier: 'qs',
|
|
defaultImport: 'qs',
|
|
});
|
|
}
|
|
}
|
|
if (requestBody) {
|
|
let content = requestBody.content;
|
|
const { $ref }: { $ref: string } = requestBody;
|
|
|
|
if (!content && $ref) {
|
|
const name = $ref.split('/').pop() as string;
|
|
content = this.openapi.components.requestBodies[name].content;
|
|
}
|
|
|
|
[contentType] = Object.keys(content);
|
|
const data = content[contentType];
|
|
|
|
const [
|
|
pType, isArray, isClass, isImport,
|
|
] = schemaParamParser(data.schema, this.openapi);
|
|
|
|
if (isImport) {
|
|
importEntities.push({ type: pType, isClass });
|
|
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
|
|
} else {
|
|
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
|
|
|
|
}
|
|
}
|
|
if (responses['200']) {
|
|
const { content, headers } = responses['200'];
|
|
if (content && (content['*/*'] || content['application/json'])) {
|
|
const { schema, examples } = content['*/*'] || content['application/json'];
|
|
|
|
if (!schema) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const propType = schemaParamParser(schema, this.openapi);
|
|
const [pType, , isClass, isImport] = propType;
|
|
|
|
if (isImport) {
|
|
importEntities.push({ type: pType, isClass });
|
|
}
|
|
hasResponseBodyType = propType;
|
|
}
|
|
}
|
|
let returnType = '';
|
|
if (hasResponseBodyType) {
|
|
const [pType, isArray, isClass] = hasResponseBodyType as any;
|
|
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
|
|
returnType = data;
|
|
} else {
|
|
returnType = 'Promise<number';
|
|
}
|
|
const shouldValidate = bodyParam.filter(b => b.isClass);
|
|
if (shouldValidate.length > 0) {
|
|
returnType += ' | string[]';
|
|
}
|
|
// append Error to default type return;
|
|
returnType += ' | Error>';
|
|
|
|
const fetcher = apiClass.addMethod({
|
|
isAsync: true,
|
|
isStatic: true,
|
|
name: operation,
|
|
returnType,
|
|
});
|
|
const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
|
|
fetcher.addParameters(params);
|
|
|
|
fetcher.setBodyText((w) => {
|
|
// Add data to URLSearchParams
|
|
if (contentType === 'text/plain') {
|
|
bodyParam.forEach((b) => {
|
|
w.writeLine(`const params = String(${b.name});`);
|
|
});
|
|
} else {
|
|
if (shouldValidate.length > 0) {
|
|
w.writeLine(`const haveError: string[] = [];`);
|
|
shouldValidate.forEach((b) => {
|
|
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
|
|
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
|
|
});
|
|
w.writeLine(`if (haveError.length > 0) {`);
|
|
w.writeLine(` return Promise.resolve(haveError);`)
|
|
w.writeLine(`}`);
|
|
}
|
|
}
|
|
// Switch return of fetch in case on queryParams
|
|
if (queryParams.length > 0) {
|
|
w.writeLine('const queryParams = {');
|
|
queryParams.forEach((q) => {
|
|
w.writeLine(` ${q.name}: ${q.name},`);
|
|
});
|
|
w.writeLine('}');
|
|
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
|
|
} else {
|
|
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
|
|
}
|
|
// Add method
|
|
w.writeLine(` method: '${method.toUpperCase()}',`);
|
|
|
|
// add Fetch options
|
|
if (contentType && contentType !== 'multipart/form-data') {
|
|
w.writeLine(' headers: {');
|
|
w.writeLine(` 'Content-Type': '${contentType}',`);
|
|
w.writeLine(' },');
|
|
}
|
|
if (contentType) {
|
|
switch (contentType) {
|
|
case 'text/plain':
|
|
w.writeLine(' body: params,');
|
|
break;
|
|
default:
|
|
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Handle response
|
|
if (hasResponseBodyType) {
|
|
w.writeLine('}).then(async (res) => {');
|
|
w.writeLine(' if (res.status === 200) {');
|
|
w.writeLine(' return res.json();');
|
|
} else {
|
|
w.writeLine('}).then(async (res) => {');
|
|
w.writeLine(' if (res.status === 200) {');
|
|
w.writeLine(' return res.status;');
|
|
}
|
|
|
|
// Handle Error
|
|
w.writeLine(' } else {');
|
|
w.writeLine(' return new Error(String(res.status));');
|
|
w.writeLine(' }');
|
|
w.writeLine('})');
|
|
});
|
|
});
|
|
|
|
const imports: any[] = [];
|
|
const types: string[] = [];
|
|
importEntities.forEach((i) => {
|
|
const { type } = i;
|
|
if (!types.includes(type)) {
|
|
imports.push(i);
|
|
types.push(type);
|
|
}
|
|
});
|
|
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
|
|
const { type: pType, isClass } = ie;
|
|
if (isClass) {
|
|
apiFile.addImportDeclaration({
|
|
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
|
defaultImport: pType,
|
|
namedImports: [`I${pType}`],
|
|
});
|
|
} else {
|
|
apiFile.addImportDeclaration({
|
|
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
|
|
namedImports: [pType],
|
|
});
|
|
}
|
|
});
|
|
|
|
this.apis.push(apiFile);
|
|
};
|
|
|
|
save = () => {
|
|
this.apis.forEach(async (e) => {
|
|
await e.saveSync();
|
|
});
|
|
};
|
|
}
|
|
|
|
|
|
export default ApiGenerator;
|