add api generator

This commit is contained in:
Vlad 2021-03-01 13:32:27 +03:00
parent 1453c27d87
commit 59a3045615
5 changed files with 261 additions and 124 deletions

View file

@ -9,4 +9,5 @@ export const trimQuotes = (str: string) => {
return str.replace(/\'|\"/g, '');
};
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';
export const BAD_REQUES_HELPER = 'BadRequesHelper';

View file

@ -11,8 +11,8 @@ const generateApi = (openApi: OpenApi) => {
const ent = new EntitiesGenerator(openApi);
ent.save();
// const api = new ApisGenerator(openApi);
// api.save();
const api = new ApisGenerator(openApi);
api.save();
}
const openApiFile = fs.readFileSync('./scripts/generator/v1.yaml', 'utf8');

View file

@ -2,15 +2,16 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import * as fs from 'fs';
import * as path from 'path';
import { stringify } from 'qs';
import { number } from 'prop-types';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';
import {
API_DIR as API_DIR_CONST,
BAD_REQUES_HELPER,
GENERATOR_ENTITY_ALLIAS,
} from '../../consts';
import { toCamel, capitalize, schemaParamParser } from './utils';
import { toCamel, capitalize, schemaParamParser, OpenApi, uncapitalize, RequestBody } from './utils';
const API_DIR = path.resolve(API_DIR_CONST);
@ -20,11 +21,15 @@ if (!fs.existsSync(API_DIR)) {
const { Project, QuoteKind } = morph;
enum PROCESS_AS {
JSON = 'JSON',
TEXT = 'TEXT',
EMPTY = 'EMPTY',
}
class ApiGenerator {
project = new Project({
tsConfigFilePath: './tsconfig.json',
addFilesFromTsConfig: false,
manipulationSettings: {
quoteKind: QuoteKind.Single,
usePrefixAndSuffixTextForRename: false,
@ -32,7 +37,7 @@ class ApiGenerator {
},
});
openapi: Record<string, any>;
openapi: OpenApi;
serverUrl: string;
@ -47,7 +52,7 @@ class ApiGenerator {
apis: morph.SourceFile[] = [];
constructor(openapi: Record<string, any>) {
constructor(openapi: OpenApi) {
this.openapi = openapi;
this.paths = openapi.paths;
this.serverUrl = openapi.servers[0].url;
@ -55,12 +60,14 @@ class ApiGenerator {
Object.keys(this.paths).forEach((pathKey) => {
Object.keys(this.paths[pathKey]).forEach((method) => {
const {
tags, operationId, parameters, responses, requestBody, security,
tags, operationId, parameters, responses, requestBody, security, "x-skip-web-api": skip
} = this.paths[pathKey][method];
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]));
if (skip) {
return;
}
if (this.controllers[controller]) {
this.controllers[controller][operationId] = {
this.controllers[controller][uncapitalize(operationId)] = {
parameters,
responses,
method,
@ -69,7 +76,7 @@ class ApiGenerator {
pathKey: pathKey.replace(/{/g, '${'),
};
} else {
this.controllers[controller] = { [operationId]: {
this.controllers[controller] = { [uncapitalize(operationId)]: {
parameters,
responses,
method,
@ -97,7 +104,7 @@ class ApiGenerator {
]);
// const schemaProperties = schemas[schemaName].properties;
const importEntities: any[] = [];
const importEntities: { type: string, isClass: boolean }[] = [];
// add api class to file
const apiClass = apiFile.addClass({
@ -111,29 +118,34 @@ class ApiGenerator {
// for each operation add fetcher
operationList.forEach((operation) => {
const {
requestBody, responses, parameters, method, pathKey, security,
requestBody, responses, parameters, method, pathKey,
} = controllerOperations[operation];
const queryParams: any[] = []; // { name, type }
const bodyParam: any[] = []; // { name, type }
const queryParams: { name: string, type: string, hasQuestionToken: boolean }[] = [];
const bodyParam: { name: string, countedType: string, type?: string, isClass?: boolean, hasQuestionToken: boolean }[] = [];
let contentType: string = '';
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);
parameters.forEach((link: {$ref: string}) => {
const temp = link.$ref.split('/').pop()
const parameter = this.openapi.components.parameters[temp!];
const {
type, isArray, isClass, isImport,
} = schemaParamParser(parameter.schema, this.openapi);
if (isImport) {
importEntities.push({ type: pType, isClass });
importEntities.push({ type, isClass });
}
if (p.in === 'query') {
if (parameter.in === 'query') {
queryParams.push({
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
name: parameter.name, type: `${type}${isArray ? '[]' : ''}`, hasQuestionToken: !parameter.required });
}
});
}
if (queryParams.length > 0) {
const imp = apiFile.getImportDeclaration((i) => {
return i.getModuleSpecifierValue() === 'qs';
@ -144,62 +156,120 @@ class ApiGenerator {
});
}
}
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;
}
const name = $ref.split('/').pop();
const { content, required } = this.openapi.components.requestBodies[name!];
[contentType] = Object.keys(content);
const data = content[contentType];
const data = content[contentType as keyof RequestBody['content']]!;
const [
pType, isArray, isClass, isImport,
] = schemaParamParser(data.schema, this.openapi);
const {
type, 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 });
importEntities.push({ type: type, isClass });
bodyParam.push({
name: type.toLowerCase(),
countedType: `${isClass ? 'I' : ''}${type}${isArray ? '[]' : ''}`,
isClass,
type,
hasQuestionToken: !required
});
} else {
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
bodyParam.push({
name: 'data',
countedType: `${type}${isArray ? '[]' : ''}`,
hasQuestionToken: !required });
}
}
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 responsesCodes = Object.keys(responses);
const responsesSchema = responsesCodes.map((code) => {
const refLink = responses[code].$ref.split('/').pop();
const ref = this.openapi.components.responses[refLink];
interface ResponseSchema {
code: number,
[PROCESS_AS.JSON]?: ReturnType<typeof schemaParamParser>;
[PROCESS_AS.TEXT]?: {
schema?: ReturnType<typeof schemaParamParser>;
xErrorCode?: string;
onlyText: boolean;
}
const propType = schemaParamParser(schema, this.openapi);
const [pType, , isClass, isImport] = propType;
if (isImport) {
importEntities.push({ type: pType, isClass });
}
hasResponseBodyType = propType;
[PROCESS_AS.EMPTY]?: boolean;
}
}
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 responseSchema: ResponseSchema = { code: Number(code) };
if (!ref.content) {
responseSchema[PROCESS_AS.EMPTY] = true;
return responseSchema;
}
if (ref.content?.['application/json']) {
const { schema } = ref.content['application/json'];
responseSchema[PROCESS_AS.JSON] = schemaParamParser(schema, this.openapi);
}
if (ref.content?.['text/palin']) {
const {
"x-error-class": xErrorClass,
"x-error-code": xErrorCode,
} = ref.content['text/palin'];
if (xErrorClass) {
const schemaLink = xErrorClass.split('/').pop();
const schema = this.openapi.components.schemas[schemaLink!];
responseSchema[PROCESS_AS.TEXT] = {
schema: schemaParamParser(schema, this.openapi),
xErrorCode,
onlyText: false,
}
} else {
responseSchema[PROCESS_AS.TEXT] = { onlyText: true };
}
}
return responseSchema;
});
let returnTypes = new Set();
bodyParam.forEach((param) => {
if (param.isClass) {
returnTypes.add(BAD_REQUES_HELPER);
importEntities.push({ type: BAD_REQUES_HELPER, isClass: true });
}
})
responsesSchema.forEach((responseSchema) => {
if (responseSchema[PROCESS_AS.JSON]) {
const { type, isClass, isImport } = responseSchema[PROCESS_AS.JSON]!;
returnTypes.add(type);
if (isImport) {
importEntities.push({ type: type, isClass });
}
}
if (responseSchema[PROCESS_AS.TEXT]) {
const { onlyText, schema } = responseSchema[PROCESS_AS.TEXT]!;
if (onlyText) {
returnTypes.add('string');
} else {
const { type, isClass, isImport } = schema!;
returnTypes.add(type);
if (isImport) {
importEntities.push({ type, isClass });
}
}
}
if (responseSchema[PROCESS_AS.EMPTY]) {
returnTypes.add('number');
}
});
returnTypes.add('undefined');
const returnType = `Promise<${Array.from(returnTypes).join(' | ')}>`;
const fetcher = apiClass.addMethod({
isAsync: true,
@ -211,23 +281,19 @@ class ApiGenerator {
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 (contentType === 'application/json') {
const shouldValidate = bodyParam.filter(b => b.isClass);
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(`haveError.push(...${b.name}.validate());`);
});
w.writeLine(`if (haveError.length > 0) {`);
w.writeLine(` return Promise.resolve(haveError);`)
w.writeLine(` return Promise.resolve(new ${BAD_REQUES_HELPER}(haveError));`)
w.writeLine(`}`);
}
}
// Switch return of fetch in case on queryParams
if (queryParams.length > 0) {
w.writeLine('const queryParams = {');
@ -243,37 +309,36 @@ class ApiGenerator {
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;
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}.serialize()` : b.name).join(', ')}),`);
}
w.writeLine('}).then(async (res) => {');
responsesSchema.forEach((responseSchema) => {
const { code } = responseSchema;
w.writeLine(` if (res.status === ${code}) {`);
if (responseSchema[PROCESS_AS.EMPTY]) {
w.writeLine(' return res.status;');
}
}
// 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(' }');
if (responseSchema[PROCESS_AS.TEXT]?.onlyText) {
w.writeLine(' return res.text();')
}
if (responseSchema[PROCESS_AS.JSON] && responseSchema[PROCESS_AS.TEXT]) {
const { type } = responseSchema[PROCESS_AS.JSON]!;
const { schema, xErrorCode } = responseSchema[PROCESS_AS.TEXT]!;
const { type: errType } = schema!;
w.writeLine(' try {');
w.writeLine(` return new ${type}(await res.json());`);
w.writeLine(' } catch {');
w.writeLine(` return new ${errType}({ msg: await res.text() code: ${xErrorCode}} as any);`);
w.writeLine(' }');
}
if (responseSchema[PROCESS_AS.JSON]) {
const { type } = responseSchema[PROCESS_AS.JSON]!;
w.writeLine(` return new ${type}(await res.json());`);
}
w.writeLine(` }`);
})
w.writeLine('})');
});
});
@ -288,17 +353,16 @@ class ApiGenerator {
}
});
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie;
const { type: type, isClass } = ie;
if (isClass) {
apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
defaultImport: pType,
namedImports: [`I${pType}`],
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
defaultImport: type,
});
} else {
apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
namedImports: [pType],
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${type}`,
namedImports: [type],
});
}
});

View file

@ -3,7 +3,7 @@ import * as path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';
import { ENT_DIR } from '../../consts';
import { ENT_DIR, BAD_REQUES_HELPER } from '../../consts';
import { TYPES, toCamel, schemaParamParser, capitalize, OpenApi, Schema } from './utils';
const { Project, QuoteKind } = morph;
@ -37,6 +37,40 @@ class EntitiesGenerator {
this.schemas = openapi.components.schemas;
this.schemaNames = Object.keys(this.schemas);
this.generateEntities();
this.generateUtils();
}
generateUtils = () => {
const helperFile = this.project.createSourceFile(`${EntDir}/${BAD_REQUES_HELPER}.ts`);
helperFile.addImportDeclaration({
moduleSpecifier: `./BadRequestResp`,
defaultImport: 'BadRequestResp',
});
helperFile.addImportDeclaration({
moduleSpecifier: `./ErrorCode`,
namedImports: ['ErrorCode'],
});
const helperClass = helperFile.addClass({
name: 'BadRequestHelper',
isDefaultExport: true,
extends: 'BadRequestResp',
properties: [{
type: 'string[]',
name: 'fields'
}]
});
const helperConstructor = helperClass.addConstructor({
parameters: [{
type: 'string[]',
name: 'fields'
}],
});
helperConstructor.setBodyText((w) => {
w.writeLine('super({ code: ErrorCode.JSN001, msg: \'Wrong fields value\' });');
w.writeLine('this.fields = fields;')
});
this.entities.push(helperFile);
}
generateEntities = () => {
@ -199,7 +233,7 @@ class EntitiesGenerator {
'',
]);
let { properties, required, allOf } = this.schemas[schemaName];
let { properties, required, allOf, $ref } = this.schemas[schemaName];
if (allOf) {
const refLink: string = allOf.find((obj: Record<string, any>) => obj.$ref).$ref;
@ -212,6 +246,38 @@ class EntitiesGenerator {
properties = newSchema.properties;
required = newSchema.required;
}
if ($ref) {
const refLink = $ref.split('/').pop()!;
entityFile.addImportDeclaration({
defaultImport: refLink,
moduleSpecifier: `./${refLink}`,
namedImports: [`I${refLink}`],
});
entityFile.addTypeAlias({
name: `I${schemaName}`,
type: `I${refLink}`,
isExported: true,
})
const entityClass = entityFile.addClass({
name: schemaName,
isDefaultExport: true,
extends: refLink,
})
const ctor = entityClass.addConstructor({
parameters: [{
name: 'props',
type: `I${schemaName}`,
}],
})
ctor.setBodyText((w) => {
w.writeLine('super(props);')
});
this.entities.push(entityFile);
return;
}
const entityInterface = entityFile.addInterface({

View file

@ -49,12 +49,12 @@ export interface Schema {
minimum?: number;
}
export interface Parametr {
export interface Parameter {
description?: string;
example?: string;
in?: 'query' | 'body' | 'headers';
name?: string;
schema?: Schema;
name: string;
schema: Schema;
required?: boolean;
}
@ -64,9 +64,6 @@ export interface RequestBody {
schema: Schema;
example?: string;
};
'text/palin'?: {
example?: string;
}
}
required?: boolean;
}
@ -78,13 +75,15 @@ export interface Response {
};
'text/palin'?: {
example?: string;
'x-error-class'?: string;
'x-error-code'?: string;
}
}
description?: string;
}
export interface Schemas {
parameters: Record<string, Parametr>;
parameters: Record<string, Parameter>;
requestBodies: Record<string, RequestBody>;
responses: Record<string, Response>;
schemas: Record<string, Schema>;
@ -93,6 +92,10 @@ export interface Schemas {
export interface OpenApi {
components: Schemas;
paths: any;
servers: {
description: string;
url: string;
}[]
}
/**
@ -118,15 +121,18 @@ const schemaParamParser = (schemaProp: Schema, openApi: OpenApi): SchemaParamPar
let isEnum = false;
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref)!.split('/');
type = (schemaProp.$ref || schemaProp.additionalProperties?.$ref)!.split('/').pop()!;
if (schemaProp.additionalProperties) {
isAdditional = true;
}
type = `${temp[temp.length - 1]}`;
const cl = openApi.components.schemas[type];
if (cl.allOf) {
const ref = cl.allOf.find((e) => !!e.$ref);
const link = schemaParamParser(ref, openApi);
return {...link, type};
}
if (cl.$ref) {
const link = schemaParamParser(cl, openApi);