diff --git a/README.md b/README.md index 35811c7..18ea4e7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # OpenAPI codegen for TypeScript ## What is this? -This is a TypeScript code generator which generates TypeScript classes and interfaces base on your [OpenAPI document](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md), including +This is a TypeScript code generator which generates TypeScript types and interfaces base on your [OpenAPI document](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md), including - `schemas` -- `IHandler`, type-defined interfaces for both server and client api -- `IServerAPI`, interface for server api -- `IClientAPI`, interface for client api -- `apiRouter`, server api prototype using [koa router](https://github.com/koajs/router) +- `IHandler`, types and interfaces for both server and client api +- `apiRouter`, server api partial implementation using [koa router](https://github.com/koajs/router) - `ClientAPI`, client api implementation using [axios](https://github.com/axios/axios) This tool assumes you use **koa router** for server and **axios** for client. @@ -59,7 +57,7 @@ module.exports = { ``` ### 3. Run this tool ``` -yarn run api-codegen [-o ] +yarn run api-codegen [-o ] [-s ] ``` The default output directory is `api/generated`. For example, if you put your api document at `api.yml`, and want the generated code put in `generated/` directory, you can execute @@ -69,34 +67,21 @@ yarn run api-codegen api.yml -o generated ``` ### 4. Implement server api ``` -import IAPI from '#api/IServerAPI'; +import {IServerAPI} from '#api/IServerAPI'; export default { - operationId: async (req, res, state, ctx) => { + operationId: async (req, state, ctx) => { // ... }, // ... -} as IAPI; +} as IServerAPI; ``` The function name is the `operationId` defined in the api document. -There are 4 arguments passed to the function. +There are 3 arguments passed to the function. #### req The request parameters and body, defined in `parameters` and `requestBody`. Any parameter will be put in `req.{in}.{name}`, where `{in}` is one of `path`, `query`, `header`, `cookie`. `requestBody` will be put in `req.body`. -#### res -The response object. -``` -// call this in the server api implementation to response -res[statusCode](responseBody); -``` -If the responseBody is not required, you can omit it or pass anything to it. -``` -// if responseBody is not required -res[statusCode](); // OK -res[statusCode]('message string'); // OK -res[statusCode]({some: 'object'}); // OK -``` #### state Alias to `ctx.state` #### ctx @@ -108,6 +93,19 @@ ctx.status = statusCode; // Do this res[statusCode](responseBody); ``` +#### return value +`[status, body]` +If the responseBody is not required, you can omit it or pass anything to it. +``` +// example +return [200, {...}]; +return [404, 'some response string']; + +// if responseBody is not required +return [statusCode]; // OK +return [statusCode, 'message string']; // OK +return [statusCode, {some: 'object'}]; // OK +``` ### 5. Mount the api router to server ``` @@ -203,18 +201,20 @@ d0.distanceFrom(new FullDate(2007, 8, 31)); // 2803 :warning: `FullDate` use `month` from 1 to 12, which differs from `Date`. Also, `FullDate` use `day` and `dayOfWeek` instead of `date` and `day`. #### Schema -It is not necessary to use `new SomeSchema(...)` of `SomeSchema.Partial(...)` to create an instance to pass to the client api. Instead, simply use an object literal. +It is not necessary to use `SomeSchema.from(...)` of `SomeSchema.Partial(...)` to create an instance to pass to the client api. Instead, simply use an object literal. -Import the schema classes from `#api/schemas`. +Import the schema type interfaces from `#api/schemas`. ``` import {SchemaA, SchemaB} from '#api/schemas'; api.postA({id: 3}, {...}) // OK, simpler -api.postA({id: 3}, new SchemaA(...)); // Well, still OK +api.postA({id: 3}, SchemaA.from(...)); // Well, still OK api.patchB({id: 3}, {...}) // OK, simpler api.patchB({id: 3}, SchemaB.Partial(...)); // Well, still OK ``` +:warning: From v2.0.0, `Schemas` are no longer class. Instead, it became a `interface` and a Object including `from`, `Partial`, and `fields` properties. + #### Handling response The api is a async function that returns a `APIPromise`, which is a extended Promise. The promise is resolved if the status code is `2xx`, otherwise it is rejected. @@ -237,6 +237,19 @@ api.getA(...) }); ``` +:warning: For any status, you can only call `.on(status, ...)` once. +``` +// OK +api.getA(...) + .on(200, a => ...) + .on(403, () => ...) + +// NG +api.getA(...) + .on(200, a => ...) + .on(200, () => ...) // Compile Error! +``` + ## Details ### Type Conversion [OpenAPI data types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#dataTypes) will be convert to TypeScript types as following: @@ -324,8 +337,8 @@ new Blob([]) // NG, Blob is not supported ``` ### Schema -Base on `#/components/schemas`, it generates class definitions and constructors in `schemas.ts`. -#### Class Definition +Base on `#/components/schemas`, it generates interface definitions and constructor functions in `schemas.ts`. +#### Interface Definition For example, ``` Post: @@ -353,7 +366,7 @@ Post: ``` will become ``` -class Post { +interface Post { id: number; ts: string; authorID: number; @@ -362,9 +375,9 @@ class Post { pinned: boolean; } ``` -#### Constructor -It also generates constructors with **strict type checking**. -The constructor takes exactly one argument with literal object type. +#### Constructor Function +It also generates constructor function `Schema.from` with **strict type checking**. +The constructor function takes exactly one argument with literal object type. If any property of the argument is not convertible to expected type, it throws an `BadValueError`. For example, @@ -383,7 +396,7 @@ NamedCircle: ``` will become ``` -class NamedCircle { +interface NamedCircle { name: string; radius: number; color: string | null; @@ -391,37 +404,37 @@ class NamedCircle { ``` Here are some examples for strict type checking: ``` -new NamedCircle({ +NamedCircle.from({ name: 'red circle', radius: 39, color: 'red', }); // OK -new NamedCircle({ +NamedCircle.from({ name: 'circle with null color', radius: 0, color: null, }); // OK, color is nullable -new NamedCircle({ +NamedCircle.from({ name: 'circle with null color', radius: 0, color: undefined, }); // Error! color should be a number or null -new NamedCircle({ +NamedCircle.from({ name: 'circle without given color', radius: 0, }); // Error! color should be given -new NamedCircle({ +NamedCircle.from({ name: 'circle with invalid radius', radius: 'miku', color: 'cyan', }); // Error! radius should be a number ``` #### Partial Function -It also generates a static function called `Partial` for initializing Partial type. +It also generates a function called `Partial` for initializing Partial type. The function also takes exactly one argument. If any property of the argument is absent or is undefined, the function will skip setting the property. However, if the property is given but not convertible to expected type, it throws a `BadValueError`. @@ -467,7 +480,14 @@ NamedCircle.Partial({ }); // Error! radius should be a number ``` -:warning: Use **object literal** if possible. The constructor and Partial function are mainly for internal use, avoid to use them unless you want to safe and strict convert a any variable to a schema type. +:warning: Use **object literal** if possible. The `from` and `Partial` function are mainly for internal use, avoid to use them unless you want to safe and strict convert a any variable to a schema type. + +### fields +`Schema` exposes its fields to `fields` constant. +Its type is `Array`. +``` +NamedCircle.fields // ['name', 'radius', 'color']: Array +``` ## Limitations ### application/json only @@ -477,6 +497,13 @@ This tool only supports `application/json` type for request and response body. A Other $ref like requestBody, responseBody are not supported currently. ## Versions +#### 2.0.0 +- simplify generated code + - merge all APIPromise class + - remove IServerAPI and IClientAPI +- remove res Object, return [status, body] in ServerAPI instead +- remove schema classes, use interface instead +- `-s` flag for `ctx.state` interface path #### 1.1.3 - expose fields of schemas to XXX.fields(static variable) #### 1.1.2 diff --git a/bin/api-codegen.js b/bin/api-codegen.js index 2d2c0fb..a38485b 100755 --- a/bin/api-codegen.js +++ b/bin/api-codegen.js @@ -9,6 +9,7 @@ const badArgv = (x, code=1) => { 'Usage: api-codegen [flags]', 'Flags:', ' -o --outputDir: outputDir', + ' -s --stateTSPath: ctx.state type definition file path', ].join('\n')); process.exit(code); }; @@ -22,6 +23,8 @@ const argAttrs = ['apiDocPath']; const flag2attr = { o: 'outputDir', outputDir: 'outputDir', + s: 'stateTSPath', + stateTSPath: 'stateTSPath', }; const requiredAttrs = [ ...argAttrs, diff --git a/dist/CodePrinter.d.ts b/dist/CodePrinter.d.ts index 702233c..7351b1d 100644 --- a/dist/CodePrinter.d.ts +++ b/dist/CodePrinter.d.ts @@ -15,7 +15,7 @@ export declare class CodePrinter { private indentString; private cIndent; constructor(writeStream?: WriteStream, indentString?: string); - writeln(s?: string, dIndent?: number): void; + writeln(s?: string, dIndent?: number, pretab?: boolean): void; write(s: string): void; tab(x: number): void; end(): Promise; diff --git a/dist/CodePrinter.js b/dist/CodePrinter.js index 6580335..b8b9d02 100644 --- a/dist/CodePrinter.js +++ b/dist/CodePrinter.js @@ -23,14 +23,15 @@ var CodePrinter = /** @class */ (function () { this.indentString = indentString; this.cIndent = 0; } - CodePrinter.prototype.writeln = function (s, dIndent) { + CodePrinter.prototype.writeln = function (s, dIndent, pretab) { if (s === void 0) { s = ''; } if (dIndent === void 0) { dIndent = 0; } - if (dIndent < 0) + if (pretab === void 0) { pretab = dIndent < 0; } + if (pretab) this.cIndent = Math.max(0, this.cIndent + dIndent); this.write(this.indentString.repeat(this.cIndent) + s + "\n"); - if (dIndent > 0) - this.cIndent += dIndent; + if (!pretab) + this.cIndent = Math.max(0, this.cIndent + dIndent); }; CodePrinter.prototype.write = function (s) { this.writeStream.write(s); diff --git a/dist/Config.d.ts b/dist/Config.d.ts index 1445378..b448437 100644 --- a/dist/Config.d.ts +++ b/dist/Config.d.ts @@ -5,14 +5,10 @@ export interface ConfigRequired { export interface ConfigOptional { interfacePrefix: string; indentString: string; - responsePrefix: string; schemasName: string; IHandlerName: string; - IServerAPIName: string; - IClientAPIName: string; ClientAPIName: string; routerName: string; - apiDirTSPath: string; ServerAPITSPath: string; utilsTSPath: string; stateTSPath: string | null; diff --git a/dist/Config.js b/dist/Config.js index 92ae0c6..2e4fdee 100644 --- a/dist/Config.js +++ b/dist/Config.js @@ -4,16 +4,12 @@ exports.configDefault = { // format interfacePrefix: 'I', indentString: ' ', - responsePrefix: '', // name schemasName: 'schemas', IHandlerName: 'IHandler', - IServerAPIName: 'IServerAPI', - IClientAPIName: 'IClientAPI', ClientAPIName: 'ClientAPI', routerName: 'apiRouter', // TS path - apiDirTSPath: '#api', ServerAPITSPath: '#ServerAPI', utilsTSPath: '@supmiku39/api-ts-gen/utils', stateTSPath: null, diff --git a/dist/OpenAPI.js b/dist/OpenAPI.js index fd21066..b461d57 100644 --- a/dist/OpenAPI.js +++ b/dist/OpenAPI.js @@ -73,7 +73,7 @@ var SchemaType = /** @class */ (function () { SchemaType.prototype.stp = function (prop, label, partial) { if (partial === void 0) { partial = false; } var stp = SchemaType.gcStp(prop, this.schema, label, partial); - return (this.required ? '' : prop + "===undefined ? undefined : ") + stp; + return (this.required ? '' : prop + "===void 0 ? void 0 : ") + stp; }; SchemaType.typeNameOf = function (schema) { var _a; @@ -124,9 +124,7 @@ var SchemaType = /** @class */ (function () { // object if (isReference(schema)) { var typeName = new SchemaType(schema, true).typeName; - return partial ? - typeName + ".Partial(" + para + ")" : - "new " + typeName + "(" + para + ")"; + return typeName + "." + (partial ? 'Partial' : 'from') + "(" + para + ")"; } // any var type = schema.type, nullable = schema.nullable, format = schema.format; diff --git a/dist/codegen.js b/dist/codegen.js index 6352346..d6d697e 100644 --- a/dist/codegen.js +++ b/dist/codegen.js @@ -5,49 +5,36 @@ var path = require("path"); var Config_1 = require("./Config"); var OpenAPI_1 = require("./OpenAPI"); var CodePrinter_1 = require("./CodePrinter"); -function codegenIServerAPI(funcs, config, cp) { - var apiDirTSPath = config.apiDirTSPath, IHandlerName = config.IHandlerName; - // import - cp.writeln("import * as IHandler from '" + apiDirTSPath + "/" + IHandlerName + "'"); - // export default - cp.writeln('\nexport default interface IAPI {', 1); - for (var _i = 0, _a = Object.keys(funcs); _i < _a.length; _i++) { - var funcName = _a[_i]; - cp.writeln(funcName + ": IHandler." + funcName + ".IServerHandler;"); - } - cp.writeln('};', -1); - return cp.end(); -} function codegenIHandler(funcs, config, cp) { - var apiDirTSPath = config.apiDirTSPath, schemasName = config.schemasName, utilsTSPath = config.utilsTSPath, responsePrefix = config.responsePrefix, validateStatus = config.validateStatus, stateTSPath = config.stateTSPath; + var schemasName = config.schemasName, utilsTSPath = config.utilsTSPath, stateTSPath = config.stateTSPath; // import - cp.writeln("import * as Schemas from '" + apiDirTSPath + "/" + schemasName + "'"); + cp.writeln("import * as Schemas from './" + schemasName + "'"); cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' + ("from '" + utilsTSPath + "'")); - cp.writeln('import {RouterContext as Context} from \'@koa/router\''); + cp.writeln('import {RouterContext as CTX} from \'@koa/router\''); cp.writeln('import {AxiosResponse} from \'axios\''); cp.writeln(stateTSPath ? "import IState from '" + stateTSPath + "'" : 'type IState = any'); - // handler types + // api req, res types + cp.writeln("export type TAPI = {", 1); for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { var _b = _a[_i], funcName = _b[0], func = _b[1]; var reqTypes = func.reqTypes, resTypes = func.resTypes, method = func.method; - cp.writeln("export namespace " + funcName + " {", 1); + cp.writeln(funcName + ": {", 1); // req - var sReqTypes = []; - // paras + // req.path, ... + cp.writeln("req: {", 1); for (var _c = 0, ELParameterIn_1 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_1.length; _c++) { var _in = ELParameterIn_1[_c]; var paras = reqTypes[_in]; if (paras == null) continue; - cp.writeln("export type T_" + _in + " = {", 1); + cp.writeln(_in + ": {", 1); for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) { var _f = _e[_d], propName = _f[0], schemaType = _f[1]; cp.writeln(schemaType.forProp(propName) + ';'); } cp.writeln('};', -1); - sReqTypes.push(_in + ": T_" + _in); } // body var body = reqTypes.body; @@ -56,76 +43,38 @@ function codegenIHandler(funcs, config, cp) { var typeName = body.typeName; if (method == 'patch') typeName = "Partial<" + typeName + ">"; - cp.writeln("export type T_body = " + typeName + ";"); - sReqTypes.push("body" + (body.required ? '' : '?') + ": T_body"); + cp.writeln("body" + (body.required ? '' : '?') + ": " + typeName + ";"); } - // IRequest - if (sReqTypes.length > 0) { - cp.writeln('interface IRequest {', 1); - for (var _g = 0, sReqTypes_1 = sReqTypes; _g < sReqTypes_1.length; _g++) { - var sReqType = sReqTypes_1[_g]; - cp.writeln(sReqType + ";"); - } - cp.writeln('}', -1); - } - else - cp.writeln('interface IRequest {}'); + cp.writeln('}', -1); // req END // res - cp.writeln('interface IResponses {', 1); - for (var _h = 0, _j = Object.entries(resTypes); _h < _j.length; _h++) { - var _k = _j[_h], status_1 = _k[0], schema = _k[1]; - cp.writeln("" + responsePrefix + status_1 + ": " + ("(" + schema.forProp('body') + ") => T;")); + cp.writeln("res: {", 1); + for (var _g = 0, _h = Object.entries(resTypes); _g < _h.length; _g++) { + var _j = _h[_g], status_1 = _j[0], schema = _j[1]; + cp.writeln(schema.required ? + schema.forProp(status_1) + ";" : status_1 + ": void;"); } + cp.writeln('}', -1); // res END + // operation END cp.writeln('}', -1); - cp.writeln('export interface IServerHandler {', 1); - cp.writeln('(req: IRequest, res: IResponses, ' + - 'state: IState, ctx: Context): void;'); - cp.writeln('}', -1); - // class _ResponsePromise - var validTypes = new Set(); - cp.writeln('export class ResponsePromise extends ' + - 'APIPromise {', 1); - // handler - cp.writeln('private handlers: Partial> = {};'); - // on - cp.writeln('on, U>(', 1); - cp.writeln('k: K, h: IResponses[K]): ResponsePromise'); - cp.tab(-1); - cp.writeln('{ const e: ResponsePromise = this; ' + - 'e.handlers[k] = h; return e; }'); - // onResponse - cp.writeln('onResponse(res: AxiosResponse){', 1); - cp.writeln('const {status, data} = res'); - cp.writeln('switch(status){', 1); - for (var _l = 0, _m = Object.entries(resTypes); _l < _m.length; _l++) { - var _o = _m[_l], status_2 = _o[0], schema = _o[1]; - // TODO void -> string or any - var isValid = validateStatus(status_2); - cp.writeln("case " + status_2 + ": return this." + (isValid ? 'onSuccess' : 'onFail') + "(this.handlers[" + status_2 + "],", 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 - var 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;"); - cp.writeln('}', -1); // end client handler - cp.writeln('}', -1); // end namespace } + // TAPI END + cp.writeln('}', -1); + // export IServerAPI + cp.writeln(''); + cp.writeln('type ValueOf = T[keyof T];'); + cp.writeln('type Dict = {[_: string]: T};'); + cp.writeln('type RServerAPI = ValueOf<', 1); + cp.writeln('{[K in keyof T]: T[K] extends void ? [K, any?] : [K, T[K]]}>;', -1, false); + cp.writeln('export type IServerAPI = {[K in keyof TAPI]:', 1); + cp.writeln("(req: TAPI[K]['req'], state: IState, ctx: CTX) =>", 1); + cp.writeln("Promise>}", -2, false); + // return return cp.end(); } function codegenRouter(funcs, config, cp) { - var apiDirTSPath = config.apiDirTSPath, schemasName = config.schemasName, responsePrefix = config.responsePrefix, ServerAPITSPath = config.ServerAPITSPath, utilsTSPath = config.utilsTSPath, stateTSPath = config.stateTSPath; + var schemasName = config.schemasName, ServerAPITSPath = config.ServerAPITSPath, utilsTSPath = config.utilsTSPath, stateTSPath = config.stateTSPath; // import - cp.writeln("import * as Schemas from '" + apiDirTSPath + "/" + schemasName + "'"); + cp.writeln("import * as Schemas from './" + 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'"); @@ -134,43 +83,24 @@ function codegenRouter(funcs, config, cp) { cp.writeln("type CTX = Router.RouterContext;"); // router cp.writeln("\nconst router = new Router();"); - 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(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(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); + var gcGetParams = { + path: function (attr) { return "ctx.params['" + attr + "']"; }, + query: function (attr) { return "ctx.query['" + attr + "']"; }, + header: function (attr) { return "ctx.headers['" + attr + "']"; }, + cookie: function (attr) { return "ctx.cookies.get('" + attr + "')"; }, + }; // route cp.writeln("\nimport api from '" + ServerAPITSPath + "'"); for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { var _b = _a[_i], funcName = _b[0], func = _b[1]; - var method = func.method, url = func.url, reqTypes = func.reqTypes, resTypes = func.resTypes; + var method = func.method, url = func.url, reqTypes = func.reqTypes; var isPartial = method === 'patch'; - var statuses = Object.keys(resTypes); // TODO escape var sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a var mid = ''; if (reqTypes.body) { - var maxSize = reqTypes.body.maxSize; + var maxSize = reqTypes.body.maxSize; // TODO doc var config_1 = maxSize == null ? '' : "{jsonLimit: '" + maxSize + "'}"; mid = "bodyParser(" + config_1 + "), "; } @@ -181,8 +111,8 @@ function codegenRouter(funcs, config, cp) { } else { cp.writeln('let req;'); - cp.writeln('const {body: reqBody} = ctx.request;'); - cp.writeln('try { req = {', 1); + cp.writeln('try {', 1); + cp.writeln('req = {', 1); // paras for (var _c = 0, ELParameterIn_2 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_2.length; _c++) { var _in = ELParameterIn_2[_c]; @@ -192,7 +122,7 @@ function codegenRouter(funcs, config, cp) { cp.writeln(_in + ": {", 1); for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) { var _f = _e[_d], name_1 = _f[0], schema = _f[1]; - var pn = "ctxGetParas." + _in + "(ctx, '" + name_1 + "')"; + var pn = gcGetParams[_in](name_1); var label = "req." + _in; cp.writeln(name_1 + ": " + schema.stp(pn, label) + ","); } @@ -201,60 +131,47 @@ function codegenRouter(funcs, config, cp) { // body var body = reqTypes.body; if (body != null) { - cp.writeln("body: " + body.stp('reqBody', 'req.body', isPartial)); + cp.writeln("body: " + body.stp('ctx.request.body', 'req.body', isPartial)); } - cp.writeln('}} catch(err) {', -1); + cp.writeln('}', -1); + cp.writeln('} catch(err) {', -1); cp.tab(1); - cp.writeln('if(err instanceof STP.BadValueError)', 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 (var _g = 0, statuses_1 = statuses; _g < statuses_1.length; _g++) { - var status_3 = statuses_1[_g]; - cp.writeln("" + responsePrefix + status_3 + ": g_res(ctx, " + status_3 + "),"); - } - cp.writeln('};', -1); // call - cp.writeln("await api." + funcName + "(req, res, ctx.state, ctx);"); - cp.writeln('})', -1); + cp.writeln("const r = await api." + funcName + "(req, ctx.state, ctx);"); + cp.writeln("ctx.status = r[0];"); + cp.writeln("ctx.body = r[1] ?? '';"); + // ctx END + cp.writeln('});', -1); } cp.writeln('\nexport default router;'); return cp.end(); } -function codegenIClientAPI(funcs, config, cp) { - var apiDirTSPath = config.apiDirTSPath, IHandlerName = config.IHandlerName; - // import - cp.writeln("import * as IHandler from '" + apiDirTSPath + "/" + IHandlerName + "'"); - // export default - cp.writeln('\nexport default interface IAPI {', 1); - cp.writeln('$baseURL: string;'); - for (var _i = 0, _a = Object.keys(funcs); _i < _a.length; _i++) { - var funcName = _a[_i]; - cp.writeln(funcName + ": IHandler." + funcName + ".IClientHandler;"); - } - cp.writeln('}', -1); - return cp.end(); -} function codegenClientAPI(funcs, config, cp) { - var apiDirTSPath = config.apiDirTSPath, IClientAPIName = config.IClientAPIName, IHandlerName = config.IHandlerName; + var IHandlerName = config.IHandlerName, schemasName = config.schemasName, utilsTSPath = config.utilsTSPath, validateStatus = config.validateStatus; // 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\''); + cp.writeln("import {TAPI} from './" + IHandlerName + "'"); + cp.writeln("import * as Schemas from './" + schemasName + "'"); + cp.writeln("import {APIPromise, StrictTypeParser as STP} from '" + utilsTSPath + "'"); + cp.writeln("import axios from 'axios'"); + cp.writeln(''); + // type + cp.writeln("type TSTP = {[K in keyof T]: (data: any) =>", 1); + cp.writeln("T[K] extends void ? any : T[K]};", -1, false); // axios - cp.writeln('\nconst $http = axios.create({', 1); + cp.writeln('const $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("url = url.replace('{'+attr+'}', value)"); cp.writeln('return url;', -1); cp.writeln('};', -1); // implementation @@ -267,38 +184,36 @@ function codegenClientAPI(funcs, config, cp) { cp.writeln('return config;'); cp.writeln('}, err => Promise.reject(err));', -1); cp.writeln('},', -1); - // functions - for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { - var _b = _a[_i], funcName = _b[0], func = _b[1]; - var ncHandler = "IHandler." + funcName; - var method = func.method, url = func.url, reqTypes = func.reqTypes; + var _loop_1 = function (funcName, func) { + var gcReq = function (_in) { return "TAPI['" + funcName + "']['req']['" + _in + "']"; }; + var method = func.method, url = func.url, reqTypes = func.reqTypes, resTypes = func.resTypes; var query = reqTypes.query, header = reqTypes.header, path_1 = reqTypes.path, body = reqTypes.body; // TODO cookie // name - cp.writeln(funcName + "(", 1); + cp.writeln(funcName + ": (", 1); // paras - for (var _c = 0, ELParameterIn_3 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_3.length; _c++) { - var _in = ELParameterIn_3[_c]; + for (var _i = 0, ELParameterIn_3 = OpenAPI_1.ELParameterIn; _i < ELParameterIn_3.length; _i++) { + var _in = ELParameterIn_3[_i]; var paras = reqTypes[_in]; if (paras == null) continue; var _required = false; - for (var _d = 0, _e = Object.values(paras); _d < _e.length; _d++) { - var required = _e[_d].required; + for (var _a = 0, _b = Object.values(paras); _a < _b.length; _a++) { + var required = _b[_a].required; if (required) { _required = true; break; } } - cp.writeln(_in + ": " + ncHandler + ".T_" + _in + (_required ? '' : '={}') + ","); + cp.writeln(_in + ": " + gcReq(_in) + (_required ? '' : '={}') + ","); } // body if (body != null) { - cp.writeln("body" + (body.required ? '' : '?') + ": " + ncHandler + ".T_body,"); + cp.writeln("body" + (body.required ? '' : '?') + ": " + gcReq('body') + ","); } - // function body + // return value cp.tab(-1); - cp.writeln("){return new " + ncHandler + - '.ResponsePromise($http({', 1); + cp.writeln(") => APIPromise.init($http({", 1); + // req cp.writeln("method: '" + method + "',"); var sURL = "'" + url + "'"; cp.writeln("url: " + (path_1 ? "urlReplacer(" + sURL + ", path)" : sURL) + ","); @@ -308,21 +223,37 @@ function codegenClientAPI(funcs, config, cp) { cp.writeln('header: header,'); if (body != null) cp.writeln('data: body,'); - cp.writeln('}));},', -1); + cp.writeln('}), {', -1); + cp.tab(1); + // stp + for (var _c = 0, _d = Object.entries(resTypes); _c < _d.length; _c++) { + var _e = _d[_c], status_2 = _e[0], schema = _e[1]; + var label = "ClientAPI[" + funcName + "][" + status_2 + "]"; + cp.writeln(status_2 + ": x => " + schema.stp('x', label) + ","); + } + cp.writeln("} as TSTP,"); + // kRsv + cp.writeln("[" + Object.keys(resTypes).filter(validateStatus).join(', ') + "]),", -1); + }; + // functions + for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { + var _b = _a[_i], funcName = _b[0], func = _b[1]; + _loop_1(funcName, func); } - cp.writeln('} as IAPI', -1); + cp.writeln('}'); return cp.end(); } function codegenSchemas(schemas, config, cp) { var utilsTSPath = config.utilsTSPath; // import cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'"); - cp.writeln(); // schema for (var _i = 0, _a = Object.entries(schemas); _i < _a.length; _i++) { var _b = _a[_i], typeName = _b[0], schema = _b[1]; + cp.writeln(); if (OpenAPI_1.isObjectSchema(schema)) { - cp.writeln("export class " + typeName + " {", 1); + // interface + cp.writeln("export interface " + typeName + " {", 1); var propTypes = []; for (var _c = 0, _d = Object.entries(schema.properties); _c < _d.length; _c++) { var _e = _d[_c], propName = _e[0], prop = _e[1]; @@ -330,28 +261,31 @@ function codegenSchemas(schemas, config, cp) { propTypes.push([propName, propType]); cp.writeln(propType.forProp(propName) + ';'); } - // constructor - cp.writeln('constructor(o: {[_: string]: any}){', 1); + cp.writeln('}', -1); // interface END + // const + cp.writeln("export const " + typeName + " = {", 1); + // .from + cp.writeln("from: (o: {[_: string]: any}): " + typeName + " => ({", 1); for (var _f = 0, propTypes_1 = propTypes; _f < propTypes_1.length; _f++) { var _g = propTypes_1[_f], n = _g[0], t = _g[1]; - cp.writeln("this." + n + " = " + t.stp("o." + n, typeName + '.' + n) + ";"); + cp.writeln(n + ": " + t.stp("o." + n, typeName + '.' + n) + ","); } - cp.writeln('}', -1); + cp.writeln('}),', -1); // Partial - cp.writeln("static Partial(o: {[_: string]: any}): Partial<" + typeName + "> {", 1); + cp.writeln("Partial: (o: {[_: string]: any}): Partial<" + typeName + "> => {", 1); cp.writeln("const r: Partial<" + typeName + "> = {};"); var locPartial = "Partial<" + typeName + ">"; for (var _h = 0, propTypes_2 = propTypes; _h < propTypes_2.length; _h++) { var _j = propTypes_2[_h], n = _j[0], t = _j[1]; - cp.writeln("if (o." + n + " !== undefined) r." + n + " = " + t.stp("o." + n, locPartial + '.' + n) + ";"); + cp.writeln("if (o." + n + " !== void 0) r." + n + " = " + t.stp("o." + n, locPartial + '.' + n) + ";"); } cp.writeln('return r;'); - cp.writeln('}', -1); + cp.writeln('},', -1); // fields - cp.writeln("static fields: Array = [", 1); + cp.writeln("fields: [", 1); cp.writeln(propTypes.map(function (e) { return "'" + e[0] + "',"; }).join(' ')); - cp.writeln(']', -1); - // end of class + cp.writeln("] as Array", -1); + // end of const cp.writeln('}', -1); } else { @@ -373,10 +307,8 @@ function codegen(openAPI, configUser) { // 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 var schemas = (_a = openAPI.components) === null || _a === void 0 ? void 0 : _a.schemas; diff --git a/dist/utils/APIPromise.d.ts b/dist/utils/APIPromise.d.ts index c4ead30..df7455e 100644 --- a/dist/utils/APIPromise.d.ts +++ b/dist/utils/APIPromise.d.ts @@ -1,13 +1,29 @@ import { AxiosResponse } from 'axios'; -declare type Optional = T | undefined | null; -declare type TPromiseOn = Optional<(_: T) => R | PromiseLike>; -export declare abstract class APIPromise implements PromiseLike { - promise: Promise; - constructor(req: Promise>); - then(onRsv?: TPromiseOn, onRjt?: TPromiseOn): Promise; - catch(onRjt: TPromiseOn): Promise; - abstract onResponse(res: AxiosResponse): T; - onSuccess(f: Optional<(x: U) => V>, v: U): U | V; - onFail(f: Optional<(x: U) => V>, v: U): V; +declare type ValueOf = T[keyof T]; +declare type RHandler = ValueOf<{ + [K in keyof T]: T[K] extends (data: any) => infer U ? U : never; +}>; +export declare class BadResponseError extends Error { + res: AxiosResponse; + constructor(res: AxiosResponse, label: string); +} +export declare class APIPromise any; +}, KOn extends keyof TRes = keyof TRes> implements PromiseLike> { + private handlers; + private promise; + constructor(resPromise: Promise, stps: { + [K in keyof TRes]: (data: any) => TRes[K]; + }, handlers: THdl); + static init(res: Promise, stps: { + [K in keyof TRes]: (data: any) => TRes[K]; + }, kRsvs: KRsv[]): APIPromise TRes[K]; + }>; + on(status: KK, handler: (data: TRes[KK]) => URst): APIPromise K extends KK ? URst : K extends keyof THdl ? ReturnType : never; + }, Exclude>; + then(onRsv?: (value: RHandler) => RRsv | PromiseLike, onRjt?: (reason: any) => RRjt | PromiseLike): Promise; + catch(onRjt: (reason: any) => RRjt | PromiseLike): Promise; } export {}; diff --git a/dist/utils/APIPromise.js b/dist/utils/APIPromise.js index 3bf5b8f..ed13877 100644 --- a/dist/utils/APIPromise.js +++ b/dist/utils/APIPromise.js @@ -13,49 +13,60 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); +function typeGuard(checker) { + return function (x) { + return checker(x); + }; +} var BadResponseError = /** @class */ (function (_super) { __extends(BadResponseError, _super); - function BadResponseError(err, res) { - var _this = _super.call(this, err.toString()) || this; - _this.err = err; + function BadResponseError(res, label) { + var _this = _super.call(this, label + " status code: " + res.status + "\ndata: " + (typeof res.data === 'object' ? JSON.stringify(res.data) : res.data)) || this; _this.res = res; Object.setPrototypeOf(_this, BadResponseError.prototype); return _this; } return BadResponseError; }(Error)); +exports.BadResponseError = BadResponseError; var APIPromise = /** @class */ (function () { - function APIPromise(req) { + function APIPromise(resPromise, stps, handlers) { var _this = this; - this.promise = new Promise(function (rsv, rjt) { - req.then(function (res) { - try { - rsv(_this.onResponse(res)); - } - catch (err) { - rjt(new BadResponseError(err, res)); - } - }).catch(function (err) { return rjt(err); }); + this.handlers = handlers; + this.promise = resPromise.then(function (res) { + var status = res.status, data = res.data; + if (!typeGuard(function (x) { return stps.hasOwnProperty(x); })(status)) { + // unexpected status + throw new BadResponseError(res, 'Unexpected'); + } + var r = stps[status](data); + if (!typeGuard(function (x) { return _this.handlers.hasOwnProperty(x); })(status)) { + // unhandled status + throw new BadResponseError(res, 'Unhandled'); + } + var handler = _this.handlers[status]; + return handler(r); }); } + APIPromise.init = function (res, stps, kRsvs) { + var handlers = {}; + for (var _i = 0, kRsvs_1 = kRsvs; _i < kRsvs_1.length; _i++) { + var kRsv = kRsvs_1[_i]; + handlers[kRsv] = function (x) { return x; }; + } + return new APIPromise(res, stps, handlers); + }; + APIPromise.prototype.on = function (status, handler) { + var self = this; + self.handlers[status] = handler; + return self; + }; APIPromise.prototype.then = function (onRsv, onRjt) { return this.promise.then(onRsv, onRjt); }; APIPromise.prototype.catch = function (onRjt) { return this.then(undefined, onRjt); }; - APIPromise.prototype.onSuccess = function (f, v) { - if (f) - return f(v); - else - return v; - }; - APIPromise.prototype.onFail = function (f, v) { - if (f) - return f(v); - else - throw new Error(); - }; return APIPromise; }()); exports.APIPromise = APIPromise; diff --git a/lib/CodePrinter.ts b/lib/CodePrinter.ts index 37fd1f5..c9c06d3 100644 --- a/lib/CodePrinter.ts +++ b/lib/CodePrinter.ts @@ -22,10 +22,10 @@ export class CodePrinter { private writeStream: WriteStream = new StringStream(), private indentString: string = ' ', ) {} - writeln(s: string = '', dIndent: number = 0) { - if (dIndent < 0) this.cIndent = Math.max(0, this.cIndent + dIndent); + writeln(s = '', dIndent = 0, pretab = dIndent<0) { + if (pretab) this.cIndent = Math.max(0, this.cIndent + dIndent); this.write(`${this.indentString.repeat(this.cIndent) + s}\n`); - if (dIndent > 0) this.cIndent += dIndent; + if (!pretab) this.cIndent = Math.max(0, this.cIndent + dIndent); } write(s: string) { this.writeStream.write(s); diff --git a/lib/Config.ts b/lib/Config.ts index 66613ad..9287e51 100644 --- a/lib/Config.ts +++ b/lib/Config.ts @@ -6,16 +6,12 @@ export interface ConfigOptional { // format interfacePrefix: string; indentString: string; - responsePrefix: string; // name schemasName: string; IHandlerName: string; - IServerAPIName: string; - IClientAPIName: string; ClientAPIName: string; routerName: string; // TS path - apiDirTSPath: string; ServerAPITSPath: string; utilsTSPath: string; stateTSPath: string | null; @@ -27,16 +23,12 @@ export const configDefault: ConfigOptional = { // format interfacePrefix: 'I', indentString: ' ', - responsePrefix: '', // name schemasName: 'schemas', IHandlerName: 'IHandler', - IServerAPIName: 'IServerAPI', - IClientAPIName: 'IClientAPI', ClientAPIName: 'ClientAPI', routerName: 'apiRouter', // TS path - apiDirTSPath: '#api', ServerAPITSPath: '#ServerAPI', utilsTSPath: '@supmiku39/api-ts-gen/utils', stateTSPath: null, diff --git a/lib/OpenAPI.ts b/lib/OpenAPI.ts index 455a111..4a2e22d 100644 --- a/lib/OpenAPI.ts +++ b/lib/OpenAPI.ts @@ -147,7 +147,7 @@ export class SchemaType { } stp(prop: string, label: string, partial: boolean=false): string { const stp = SchemaType.gcStp(prop, this.schema, label, partial); - return (this.required ? '' : `${prop}===undefined ? undefined : `)+stp; + return (this.required ? '' : `${prop}===void 0 ? void 0 : `)+stp; } private schema: Schema | Reference; @@ -195,9 +195,7 @@ export class SchemaType { // object if (isReference(schema)) { const typeName = new SchemaType(schema, true).typeName; - return partial ? - `${typeName}.Partial(${para})` : - `new ${typeName}(${para})`; + return `${typeName}.${partial ? 'Partial': 'from'}(${para})`; } // any const {type, nullable, format} = schema; diff --git a/lib/codegen.ts b/lib/codegen.ts index 75da415..3fd394f 100644 --- a/lib/codegen.ts +++ b/lib/codegen.ts @@ -7,50 +7,34 @@ import { } 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, + schemasName, utilsTSPath, stateTSPath, } = config; // import - cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`); + cp.writeln(`import * as Schemas from './${schemasName}'`); cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' + `from '${utilsTSPath}'`); - cp.writeln('import {RouterContext as Context} from \'@koa/router\''); + cp.writeln('import {RouterContext as CTX} from \'@koa/router\''); cp.writeln('import {AxiosResponse} from \'axios\''); cp.writeln(stateTSPath ? `import IState from '${stateTSPath}'` : 'type IState = any'); - // handler types + // api req, res types + cp.writeln(`export type TAPI = {`, 1); for (const [funcName, func] of Object.entries(funcs)) { const {reqTypes, resTypes, method} = func; - cp.writeln(`export namespace ${funcName} {`, 1); + cp.writeln(`${funcName}: {`, 1); // req - const sReqTypes: string[] = []; - // paras + // req.path, ... + cp.writeln(`req: {`, 1); for (const _in of ELParameterIn) { const paras = reqTypes[_in]; if (paras == null) continue; - cp.writeln(`export type T_${_in} = {`, 1); + cp.writeln(`${_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; @@ -58,75 +42,40 @@ function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) { // 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`); + cp.writeln(`body${body.required ? '' : '?'}: ${typeName};`); } - // 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 {}'); + cp.writeln('}', -1); // req END // res - cp.writeln('interface IResponses {', 1); + cp.writeln(`res: {`, 1); for (const [status, schema] of Object.entries(resTypes)) { - cp.writeln(`${responsePrefix}${status}: ${ - `(${schema.forProp('body')}) => T;` - }`); + cp.writeln(schema.required ? + `${schema.forProp(status)};`: `${status}: void;`); } + cp.writeln('}', -1); // res END + // operation END cp.writeln('}', -1); - cp.writeln('export interface IServerHandler {', 1); - cp.writeln('(req: IRequest, res: IResponses, ' + - 'state: IState, ctx: Context): void;'); - cp.writeln('}', -1); - // class _ResponsePromise - const validTypes = new Set(); - cp.writeln('export class ResponsePromise extends ' + - 'APIPromise {', 1); - // handler - cp.writeln('private handlers: Partial> = {};'); - // on - cp.writeln('on, U>(', 1); - cp.writeln('k: K, h: IResponses[K]): ResponsePromise'); - cp.tab(-1); - cp.writeln('{ const e: ResponsePromise = this; ' + - 'e.handlers[k] = h; return e; }'); - // onResponse - cp.writeln('onResponse(res: AxiosResponse){', 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;`); - cp.writeln('}', -1); // end client handler - cp.writeln('}', -1); // end namespace } + // TAPI END + cp.writeln('}', -1); + // export IServerAPI + cp.writeln(''); + cp.writeln('type ValueOf = T[keyof T];'); + cp.writeln('type Dict = {[_: string]: T};'); + cp.writeln('type RServerAPI = ValueOf<', 1); + cp.writeln('{[K in keyof T]: T[K] extends void ? [K, any?] : [K, T[K]]}>;', + -1, false); + cp.writeln('export type IServerAPI = {[K in keyof TAPI]:', 1); + cp.writeln(`(req: TAPI[K]['req'], state: IState, ctx: CTX) =>`, 1); + cp.writeln(`Promise>}`, -2, false); + // return return cp.end(); } function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) { const { - apiDirTSPath, schemasName, responsePrefix, - ServerAPITSPath, utilsTSPath, stateTSPath, + schemasName, ServerAPITSPath, utilsTSPath, stateTSPath, } = config; // import - cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`); + cp.writeln(`import * as Schemas from './${schemasName}'`); cp.writeln(`import * as Router from '@koa/router'`); cp.writeln( `import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`); @@ -136,44 +85,25 @@ function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) { cp.writeln(`type CTX = Router.RouterContext;`); // router cp.writeln(`\nconst router = new Router();`); - 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(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(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); + const gcGetParams = { + path: (attr: string) => `ctx.params['${attr}']`, + query: (attr: string) => `ctx.query['${attr}']`, + header: (attr: string) => `ctx.headers['${attr}']`, + cookie: (attr: string) => `ctx.cookies.get('${attr}')`, + }; // route cp.writeln(`\nimport api from '${ServerAPITSPath}'`); for (const [funcName, func] of Object.entries(funcs)) { const { - method, url, reqTypes, resTypes, + method, url, reqTypes, } = 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 {maxSize} = reqTypes.body; // TODO doc const config = maxSize == null ? '' : `{jsonLimit: '${maxSize}'}`; mid = `bodyParser(${config}), `; } @@ -183,15 +113,15 @@ function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) { cp.writeln('const req = {};'); } else { cp.writeln('let req;'); - cp.writeln('const {body: reqBody} = ctx.request;'); - cp.writeln('try { req = {', 1); + cp.writeln('try {', 1); + cp.writeln('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 pn = gcGetParams[_in](name); const label = `req.${_in}`; cp.writeln(`${name}: ${schema.stp(pn, label)},`); } @@ -200,62 +130,48 @@ function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) { // body const {body} = reqTypes; if (body != null) { - cp.writeln(`body: ${body.stp('reqBody', 'req.body', isPartial)}`); + cp.writeln( + `body: ${body.stp('ctx.request.body', 'req.body', isPartial)}`); } - cp.writeln('}} catch(err) {', -1); cp.tab(1); - cp.writeln('if(err instanceof STP.BadValueError)', 1); + cp.writeln('}', -1); + 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(`const r = await api.${funcName}(req, ctx.state, ctx);`); + cp.writeln(`ctx.status = r[0];`); + cp.writeln(`ctx.body = r[1] ?? '';`); + // ctx END + 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; + const {IHandlerName, schemasName, utilsTSPath, validateStatus} = 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\''); + cp.writeln(`import {TAPI} from './${IHandlerName}'`); + cp.writeln(`import * as Schemas from './${schemasName}'`); + cp.writeln( + `import {APIPromise, StrictTypeParser as STP} from '${utilsTSPath}'`); + cp.writeln(`import axios from 'axios'`); + cp.writeln(''); + // type + cp.writeln(`type TSTP = {[K in keyof T]: (data: any) =>`, 1); + cp.writeln(`T[K] extends void ? any : T[K]};`, -1, false); // axios - cp.writeln('\nconst $http = axios.create({', 1); + cp.writeln('const $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(`url = url.replace('{'+attr+'}', value)`); cp.writeln('return url;', -1); cp.writeln('};', -1); // implementation @@ -270,13 +186,13 @@ function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) { cp.writeln('},', -1); // functions for (const [funcName, func] of Object.entries(funcs)) { - const ncHandler = `IHandler.${funcName}`; - const {method, url, reqTypes} = func; + const gcReq = (_in: string) => `TAPI['${funcName}']['req']['${_in}']`; + const {method, url, reqTypes, resTypes} = func; const { query, header, path, body, } = reqTypes; // TODO cookie // name - cp.writeln(`${funcName}(`, 1); + cp.writeln(`${funcName}: (`, 1); // paras for (const _in of ELParameterIn) { const paras = reqTypes[_in]; @@ -287,25 +203,35 @@ function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) { _required = true; break; } } - cp.writeln(`${_in}: ${ncHandler}.T_${_in}${_required ? '' : '={}'},`); + cp.writeln(`${_in}: ${gcReq(_in)}${_required ? '' : '={}'},`); } // body if (body != null) { - cp.writeln(`body${body.required ? '' : '?'}: ${ncHandler}.T_body,`); + cp.writeln(`body${body.required ? '' : '?'}: ${gcReq('body')},`); } - // function body + // return value cp.tab(-1); - cp.writeln(`){return new ${ncHandler}`+ - '.ResponsePromise($http({', 1); + cp.writeln(`) => APIPromise.init($http({`, 1); + // req 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('}), {', -1); cp.tab(1); + // stp + for (const [status, schema] of Object.entries(resTypes)) { + const label = `ClientAPI[${funcName}][${status}]`; + cp.writeln(`${status}: x => ${schema.stp('x', label)},`); + } + cp.writeln(`} as TSTP,`); + // kRsv + cp.writeln(`[${ + Object.keys(resTypes).filter(validateStatus).join(', ') + }]),`, -1); } - cp.writeln('} as IAPI', -1); + cp.writeln('}'); return cp.end(); } @@ -314,39 +240,43 @@ function codegenSchemas(schemas: Schemas, config: Config, cp: CodePrinter) { // import cp.writeln( `import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`); - cp.writeln(); // schema for (const [typeName, schema] of Object.entries(schemas)) { + cp.writeln(); if (isObjectSchema(schema)) { - cp.writeln(`export class ${typeName} {`, 1); + // interface + cp.writeln(`export interface ${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)+';'); } - // constructor - cp.writeln('constructor(o: {[_: string]: any}){', 1); + cp.writeln('}', -1); // interface END + // const + cp.writeln(`export const ${typeName} = {`, 1); + // .from + cp.writeln(`from: (o: {[_: string]: any}): ${typeName} => ({`, 1); for (const [n, t] of propTypes) { - cp.writeln(`this.${n} = ${t.stp(`o.${n}`, typeName+'.'+n)};`); + cp.writeln(`${n}: ${t.stp(`o.${n}`, typeName+'.'+n)},`); } - cp.writeln('}', -1); + cp.writeln('}),', -1); // Partial cp.writeln( - `static Partial(o: {[_: string]: any}): Partial<${typeName}> {`, 1); + `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} = ${ + cp.writeln(`if (o.${n} !== void 0) r.${n} = ${ t.stp(`o.${n}`, locPartial+'.'+n)};`); } cp.writeln('return r;'); - cp.writeln('}', -1); + cp.writeln('},', -1); // fields - cp.writeln(`static fields: Array = [`, 1); + cp.writeln(`fields: [`, 1); cp.writeln(propTypes.map(e => `'${e[0]}',`).join(' ')); - cp.writeln(']', -1); - // end of class + cp.writeln(`] as Array`, -1); + // end of const cp.writeln('}', -1); } else { cp.writeln(`export type ${typeName} = ${SchemaType.typeNameOf(schema)}`); @@ -370,10 +300,8 @@ export default function codegen(openAPI: OpenAPI, configUser: ConfigUser) { // 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; diff --git a/lib/utils/APIPromise.ts b/lib/utils/APIPromise.ts index 44611f6..27a9daa 100644 --- a/lib/utils/APIPromise.ts +++ b/lib/utils/APIPromise.ts @@ -1,43 +1,89 @@ import {AxiosResponse} from 'axios'; -class BadResponseError extends Error { - constructor(public err: Error, public res: AxiosResponse) { - super(err.toString()); +type ValueOf = T[keyof T]; +type RHandler = ValueOf<{[K in keyof T]: + T[K] extends (data: any) => infer U ? U : never}>; + +function typeGuard(checker: (x: U) => boolean) { + return function(x: U): x is T { + return checker(x); + }; +} + +export class BadResponseError extends Error { + constructor(public res: AxiosResponse, label: string) { + super(`${label} status code: ${res.status}\ndata: ${ + typeof res.data === 'object' ? JSON.stringify(res.data) : res.data}`); Object.setPrototypeOf(this, BadResponseError.prototype); } } -type Optional = T | undefined | null; -type TPromiseOn = Optional<(_: T) => R | PromiseLike>; -export abstract class APIPromise implements PromiseLike { - promise: Promise; +export class APIPromise< + TRes, + KRsv extends keyof TRes, + THdl extends {[K in KRsv]: (data: TRes[K]) => any}, + KOn extends keyof TRes = keyof TRes, +> implements PromiseLike> { + private promise: Promise> - constructor(req: Promise>) { - this.promise = new Promise((rsv, rjt)=>{ - req.then(res=>{ - try { - rsv(this.onResponse(res)); - } catch (err) { - rjt(new BadResponseError(err, res)); - } - }).catch(err=>rjt(err)); + constructor( + resPromise: Promise, + stps: {[K in keyof TRes]: (data: any) => TRes[K]}, + private handlers: THdl, + ) { + this.promise = resPromise.then(res => { + const {status, data} = res; + if (!typeGuard(x=>stps.hasOwnProperty(x))(status)) { + // unexpected status + throw new BadResponseError(res, 'Unexpected'); + } + const r = stps[status](data); + if (!typeGuard(x=>this.handlers.hasOwnProperty(x))(status)) { + // unhandled status + throw new BadResponseError(res, 'Unhandled'); + } + const handler = this.handlers[status]; + return handler(r); }); } - then(onRsv?: TPromiseOn, onRjt?: TPromiseOn) { - return this.promise.then(onRsv, onRjt); - } - catch(onRjt: TPromiseOn) { - return this.then(undefined, onRjt); + static init( + res: Promise, + stps: {[K in keyof TRes]: (data: any) => TRes[K]}, + kRsvs: KRsv[], + ): APIPromise< + TRes, KRsv, {[K in KRsv]: (data: TRes[K]) => TRes[K]} + > { + const handlers: {[K in KRsv]: (data: TRes[K]) => TRes[K]} = {} as any; + for (const kRsv of kRsvs) { + handlers[kRsv] = x => x; + } + return new APIPromise(res, stps, handlers); } - abstract onResponse(res: AxiosResponse): T; - onSuccess(f: Optional<(x: U)=>V>, v: U): U | V { - if (f) return f(v); - else return v; + on( + status: KK, handler: (data: TRes[KK]) => URst, + ): APIPromise< + TRes, + KRsv | KK, + {[K in (KRsv | KK)]: (data: TRes[K]) => K extends KK ? URst : + K extends keyof THdl ? ReturnType: never}, + Exclude + > { + const self = this as any; + self.handlers[status] = handler; + return self; } - onFail(f: Optional<(x: U)=>V>, v: U) { - if (f) return f(v); - else throw new Error(); + + then( + onRsv?: (value: RHandler) => RRsv|PromiseLike, + onRjt?: (reason: any) => RRjt|PromiseLike, + ): Promise { + return this.promise.then(onRsv, onRjt); + } + catch( + onRjt: (reason: any) => RRjt|PromiseLike, + ) { + return this.then(undefined, onRjt); } } diff --git a/package.json b/package.json index d8ef7a0..a425c2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@supmiku39/api-ts-gen", - "version": "1.1.3", + "version": "2.0.0", "description": "OpenAPI code generator for TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts",