Archived
1
0
Fork 0
This repository has been archived on 2024-02-06. You can view files and clone it, but cannot push or open issues or pull requests.
api-ts-gen/lib/codegen.ts
supmiku39 87615616d6 fix Partial constructor, enhance error msg
add more info to BadValueError
  special message on undefined and null
  trace the location where the error is thrown
add int32 STP
fix STP null argument bug
2020-04-08 20:17:57 +09:00

381 lines
14 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import {Config, ConfigUser, configDefault} from './Config';
import {
apiFunctionsOf, OpenAPI, APIFunctions as APIFuncs,
ELParameterIn, SchemaType, Schemas, isObjectSchema,
} from './OpenAPI';
import {CodePrinter} from './CodePrinter';
function codegenIServerAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const {apiDirTSPath, IHandlerName} = config;
// import
cp.writeln(`import * as IHandler from '${apiDirTSPath}/${IHandlerName}'`);
// export default
cp.writeln('\nexport default interface IAPI {', 1);
for (const funcName of Object.keys(funcs)) {
cp.writeln(
`${funcName}: IHandler.${funcName}.IServerHandler;`,
);
}
cp.writeln('};', -1);
return cp.end();
}
function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const {
apiDirTSPath, schemasName, utilsTSPath,
responsePrefix, validateStatus, stateTSPath,
} = config;
// import
cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`);
cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' +
`from '${utilsTSPath}'`);
cp.writeln('import {RouterContext as Context} from \'@koa/router\'');
cp.writeln('import {AxiosResponse} from \'axios\'');
cp.writeln(stateTSPath ?
`import IState from '${stateTSPath}'` : 'type IState = any');
// handler types
for (const [funcName, func] of Object.entries(funcs)) {
const {reqTypes, resTypes, method} = func;
cp.writeln(`export namespace ${funcName} {`, 1);
// req
const sReqTypes: string[] = [];
// paras
for (const _in of ELParameterIn) {
const paras = reqTypes[_in];
if (paras == null) continue;
cp.writeln(`export type T_${_in} = {`, 1);
for (const [propName, schemaType] of Object.entries(paras)) {
cp.writeln(schemaType.forProp(propName)+';');
}
cp.writeln('};', -1);
sReqTypes.push(`${_in}: T_${_in}`);
}
// body
const {body} = reqTypes;
if (body != null) {
// PATCH's req body: Partial
let {typeName} = body;
if (method == 'patch') typeName = `Partial<${typeName}>`;
cp.writeln(`export type T_body = ${typeName};`);
sReqTypes.push(`body${body.required ? '' : '?'}: T_body`);
}
// IRequest
if (sReqTypes.length > 0) {
cp.writeln('interface IRequest {', 1);
for (const sReqType of sReqTypes) cp.writeln(`${sReqType};`);
cp.writeln('}', -1);
} else cp.writeln('interface IRequest {}');
// res
cp.writeln('interface IResponses<T> {', 1);
for (const [status, schema] of Object.entries(resTypes)) {
cp.writeln(`${responsePrefix}${status}: ${
`(${schema.forProp('body')}) => T;`
}`);
}
cp.writeln('}', -1);
cp.writeln('export interface IServerHandler {', 1);
cp.writeln('(req: IRequest, res: IResponses<void>, ' +
'state: IState, ctx: Context): void;');
cp.writeln('}', -1);
// class _ResponsePromise
const validTypes = new Set<string>();
cp.writeln('export class ResponsePromise<T> extends ' +
'APIPromise<T|T_ValidResponse> {', 1);
// handler
cp.writeln('private handlers: Partial<IResponses<T>> = {};');
// on
cp.writeln('on<K extends keyof IResponses<T>, U>(', 1);
cp.writeln('k: K, h: IResponses<U>[K]): ResponsePromise<T|U>');
cp.tab(-1);
cp.writeln('{ const e: ResponsePromise<T|U> = this; ' +
'e.handlers[k] = h; return e; }');
// onResponse
cp.writeln('onResponse(res: AxiosResponse<any>){', 1);
cp.writeln('const {status, data} = res');
cp.writeln('switch(status){', 1);
for (const [status, schema] of Object.entries(resTypes)) {
// TODO void -> string or any
const isValid = validateStatus(status);
cp.writeln(`case ${status}: return this.${
isValid ? 'onSuccess' : 'onFail'
}(this.handlers[${status}],`, 1);
cp.writeln(`${schema.stp('data', 'res.body')});`);
cp.tab(-1);
if (isValid) validTypes.add(schema.typeName);
}
cp.writeln('}', -1); // end switch
cp.writeln('throw new Error(\'Unexpect status code: \'+status);');
cp.writeln('}', -1); // end onResponse
cp.writeln('}', -1); // end class
// valid type
const sValidTypes = Array.from(validTypes.values()).join(' | ');
cp.writeln(`export type T_ValidResponse = ${sValidTypes};`);
// export client handler
cp.writeln('export interface IClientHandler {', 1);
cp.writeln(`(${sReqTypes.join(', ')}): ResponsePromise<never>;`);
cp.writeln('}', -1); // end client handler
cp.writeln('}', -1); // end namespace
}
return cp.end();
}
function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const {
apiDirTSPath, schemasName, responsePrefix,
ServerAPITSPath, utilsTSPath, stateTSPath,
} = config;
// import
cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`);
cp.writeln(`import * as Router from '@koa/router'`);
cp.writeln(
`import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`);
cp.writeln(`import * as bodyParser from 'koa-body'`);
cp.writeln(stateTSPath ?
`import IState from '${stateTSPath}'` : 'type IState = any');
cp.writeln(`type CTX = Router.RouterContext<IState>;`);
// router
cp.writeln(`\nconst router = new Router<IState>();`);
cp.writeln('');
// function
cp.writeln('function isEmpty(x: any): boolean {', 1);
cp.writeln('if(x == null || x === \'\') return true;');
cp.writeln('if(typeof x === \'object\') return Object.keys(x).length===0');
cp.writeln('return false;');
cp.writeln('}', -1);
cp.writeln('function nullableParse<T>(v: any, ' +
'p: (x: any)=>T): T | undefined {', 1);
cp.writeln('return isEmpty(v) ? undefined : p(v);');
cp.writeln('}', -1);
cp.writeln('const ctxGetParas = {', 1);
cp.writeln('path: (ctx: CTX, attr: string) => ctx.params[attr],');
cp.writeln('query: (ctx: CTX, attr: string) => ctx.query[attr],');
cp.writeln('header: (ctx: CTX, attr: string) => ctx.headers[attr],');
cp.writeln('cookie: (ctx: CTX, attr: string) => ctx.cookies.get(attr),');
cp.writeln('};', -1);
// response generator
cp.writeln('function g_res<T>(ctx: CTX, ' +
'status: number, dft: string = \'\'){', 1);
cp.writeln('return (body: T) => {', 1);
cp.writeln('ctx.status = status;');
cp.writeln('ctx.body = body ?? dft;');
cp.writeln('}', -1);
cp.writeln('}', -1);
// route
cp.writeln(`\nimport api from '${ServerAPITSPath}'`);
for (const [funcName, func] of Object.entries(funcs)) {
const {
method, url, reqTypes, resTypes,
} = func;
const isPartial = method === 'patch';
const statuses = Object.keys(resTypes);
// TODO escape
const sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a
let mid = '';
if (reqTypes.body) {
const {maxSize} = reqTypes.body;
const config = maxSize == null ? '' : `{jsonLimit: '${maxSize}'}`;
mid = `bodyParser(${config}), `;
}
cp.writeln(`router.${method}('${sURL}', ${mid}async ctx => {`, 1);
// req
if (Object.keys(reqTypes).length === 0) {
cp.writeln('const req = {};');
} else {
cp.writeln('let req;');
cp.writeln('const {body: reqBody} = ctx.request;');
cp.writeln('try { req = {', 1);
// paras
for (const _in of ELParameterIn) {
const paras = reqTypes[_in];
if (paras == null) continue;
cp.writeln(`${_in}: {`, 1);
for (const [name, schema] of Object.entries(paras)) {
const pn = `ctxGetParas.${_in}(ctx, '${name}')`;
const label = `req.${_in}`;
cp.writeln(`${name}: ${schema.stp(pn, label)},`);
}
cp.writeln('},', -1);
}
// body
const {body} = reqTypes;
if (body != null) {
cp.writeln(`body: ${body.stp('reqBody', 'req.body', isPartial)}`);
}
cp.writeln('}} catch(err) {', -1); cp.tab(1);
cp.writeln('if(err instanceof STP.BadValueError)', 1);
cp.writeln('return ctx.throw(400, err.toString());'); cp.tab(-1);
cp.writeln('throw err;');
cp.writeln('}', -1);
}
// res
cp.writeln('const res = {', 1);
for (const status of statuses) {
cp.writeln(`${responsePrefix}${status}: g_res(ctx, ${status}),`);
}
cp.writeln('};', -1);
// call
cp.writeln(`await api.${funcName}(req, res, ctx.state, ctx);`);
cp.writeln('})', -1);
}
cp.writeln('\nexport default router;');
return cp.end();
}
function codegenIClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const {apiDirTSPath, IHandlerName} = config;
// import
cp.writeln(`import * as IHandler from '${apiDirTSPath}/${IHandlerName}'`);
// export default
cp.writeln('\nexport default interface IAPI {', 1);
cp.writeln('$baseURL: string;');
for (const funcName of Object.keys(funcs)) {
cp.writeln(
`${funcName}: IHandler.${funcName}.IClientHandler;`,
);
}
cp.writeln('}', -1);
return cp.end();
}
function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const {
apiDirTSPath, IClientAPIName, IHandlerName,
} = config;
// import
cp.writeln(`import * as _IAPI from '${apiDirTSPath}/${IClientAPIName}'`);
cp.writeln(`import IAPI from '${apiDirTSPath}/${IClientAPIName}'`);
cp.writeln(`import * as IHandler from '${apiDirTSPath}/${IHandlerName}'`);
cp.writeln('import axios from \'axios\'');
// axios
cp.writeln('\nconst $http = axios.create({', 1);
cp.writeln('validateStatus: ()=>true,');
cp.writeln('});', -1);
// function
cp.writeln('\nfunction urlReplacer(url: string, ' +
'rules: {[_: string]: any}): string {', 1);
cp.writeln('for(const [attr, value] of Object.entries(rules))', 1);
cp.writeln('url = url.replace(\'{\'+attr+\'}\', value)');
cp.writeln('return url;', -1);
cp.writeln('};', -1);
// implementation
// export default
cp.writeln('\nexport default {', 1);
// set $baseURL
cp.writeln('set $baseURL(url: string) {', 1);
cp.writeln('$http.interceptors.request.use(async config => {', 1);
cp.writeln('config.baseURL = url;');
cp.writeln('return config;', -1);
cp.writeln('}, err => Promise.reject(err));', -1);
cp.writeln('},');
// functions
for (const [funcName, func] of Object.entries(funcs)) {
const ncHandler = `IHandler.${funcName}`;
const {method, url, reqTypes} = func;
const {
query, header, path, body,
} = reqTypes; // TODO cookie
// name
cp.writeln(`${funcName}(`, 1);
// paras
for (const _in of ELParameterIn) {
const paras = reqTypes[_in];
if (paras == null) continue;
let _required = false;
for (const {required} of Object.values(paras)) {
if (required) {
_required = true; break;
}
}
cp.writeln(`${_in}: ${ncHandler}.T_${_in}${_required ? '' : '={}'},`);
}
// body
if (body != null) {
cp.writeln(`body${body.required ? '' : '?'}: ${ncHandler}.T_body,`);
}
// function body
cp.tab(-1);
cp.writeln(`){return new ${ncHandler}`+
'.ResponsePromise<never>($http({', 1);
cp.writeln(`method: '${method}',`);
const sURL = `'${url}'`;
cp.writeln(`url: ${path ? `urlReplacer(${sURL}, path)` : sURL},`);
if (query) cp.writeln('params: query,');
if (header) cp.writeln('header: header,');
if (body != null) cp.writeln('data: body,');
cp.writeln('}));},', -1);
}
cp.writeln('} as IAPI', -1);
return cp.end();
}
function codegenSchemas(schemas: Schemas, config: Config, cp: CodePrinter) {
const {utilsTSPath} = config;
// import
cp.writeln(
`import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`);
cp.writeln();
// schema
for (const [typeName, schema] of Object.entries(schemas)) {
if (isObjectSchema(schema)) {
cp.writeln(`export class ${typeName} {`, 1);
const propTypes: [string, SchemaType][] = [];
for (const [propName, prop] of Object.entries(schema.properties)) {
const propType = new SchemaType(prop, true); // TODO required
propTypes.push([propName, propType]);
cp.writeln(propType.forProp(propName)+';');
}
// method
cp.writeln('constructor(o: {[_: string]: any}){', 1);
for (const [n, t] of propTypes) {
cp.writeln(`this.${n} = ${t.stp(`o.${n}`, typeName+'.'+n)};`);
}
cp.writeln('}', -1);
// Partial
cp.writeln(
`static Partial(o: {[_: string]: any}): Partial<${typeName}> {`, 1);
cp.writeln(`const r: Partial<${typeName}> = {};`);
const locPartial = `Partial<${typeName}>`;
for (const [n, t] of propTypes) {
cp.writeln(`if (o.${n} !== undefined) r.${n} = ${
t.stp(`o.${n}`, locPartial+'.'+n)};`);
}
cp.writeln('return r;');
cp.writeln('}', -1);
// end of class
cp.writeln('}', -1);
} else {
cp.writeln(`export type ${typeName} = ${SchemaType.typeNameOf(schema)}`);
}
}
// return
return cp.end();
}
export default function codegen(openAPI: OpenAPI, configUser: ConfigUser) {
const config: Config = Object.assign({}, configUser, configDefault);
// prepare
fs.mkdirSync(config.outputDir, {recursive: true});
const apiFuncs = apiFunctionsOf(openAPI);
const gCP = (fn: string) => new CodePrinter(
fs.createWriteStream(path.join(config.outputDir, fn+'.ts')),
config.indentString,
);
const ps: Promise<any>[] = [];
// write files
// handler
ps.push(codegenIHandler(apiFuncs, config, gCP(config.IHandlerName)));
// server
ps.push(codegenIServerAPI(apiFuncs, config, gCP(config.IServerAPIName)));
ps.push(codegenRouter(apiFuncs, config, gCP(config.routerName)));
// client
ps.push(codegenIClientAPI(apiFuncs, config, gCP(config.IClientAPIName)));
ps.push(codegenClientAPI(apiFuncs, config, gCP(config.ClientAPIName)));
// schema
const schemas = openAPI.components?.schemas;
if (schemas != null) {
ps.push(codegenSchemas(schemas, config, gCP(config.schemasName)));
}
// return
return Promise.all(ps);
}