Archived
1
0
Fork 0

[v2.0.0] simplify generated code, change some types

- 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
This commit is contained in:
supmiku39 2020-04-15 07:45:51 +09:00
parent c482e91cbc
commit d76000a1e4
16 changed files with 429 additions and 485 deletions

107
README.md
View file

@ -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 <your-openapi-document> [-o <output-dir>]
yarn run api-codegen <your-openapi-document> [-o <output-dir>] [-s <ctx.state-interface-path>]
```
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<keyof Schema>`.
```
NamedCircle.fields // ['name', 'radius', 'color']: Array<keyof NamedCircle>
```
## 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

View file

@ -9,6 +9,7 @@ const badArgv = (x, code=1) => {
'Usage: api-codegen <apiDocPath> [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,

View file

@ -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<void>;

9
dist/CodePrinter.js vendored
View file

@ -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);

4
dist/Config.d.ts vendored
View file

@ -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;

4
dist/Config.js vendored
View file

@ -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,

6
dist/OpenAPI.js vendored
View file

@ -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;

286
dist/codegen.js vendored
View file

@ -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<T> {', 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<void>, ' +
'state: IState, ctx: Context): void;');
cp.writeln('}', -1);
// class _ResponsePromise
var validTypes = new Set();
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 (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<never>;");
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> = T[keyof T];');
cp.writeln('type Dict<T> = {[_: string]: T};');
cp.writeln('type RServerAPI<T> = 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<RServerAPI<TAPI[K]['res']>>}", -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<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);
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<T> = {[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<never>($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<TAPI['" + funcName + "']['res']>,");
// 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<keyof " + typeName + "> = [", 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<keyof " + typeName + ">", -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;

View file

@ -1,13 +1,29 @@
import { AxiosResponse } from 'axios';
declare type Optional<T> = T | undefined | null;
declare type TPromiseOn<T, R> = Optional<(_: T) => R | PromiseLike<R>>;
export declare abstract class APIPromise<T> implements PromiseLike<T> {
promise: Promise<T>;
constructor(req: Promise<AxiosResponse<any>>);
then<T1 = T, T2 = never>(onRsv?: TPromiseOn<T, T1>, onRjt?: TPromiseOn<any, T2>): Promise<T1 | T2>;
catch<T2>(onRjt: TPromiseOn<any, T2>): Promise<T | T2>;
abstract onResponse(res: AxiosResponse<any>): T;
onSuccess<U, V>(f: Optional<(x: U) => V>, v: U): U | V;
onFail<U, V>(f: Optional<(x: U) => V>, v: U): V;
declare type ValueOf<T> = T[keyof T];
declare type RHandler<T> = ValueOf<{
[K in keyof T]: T[K] extends (data: any) => infer U ? U : never;
}>;
export declare class BadResponseError extends Error {
res: AxiosResponse<any>;
constructor(res: AxiosResponse<any>, label: string);
}
export declare class APIPromise<TRes, KRsv extends keyof TRes, THdl extends {
[K in KRsv]: (data: TRes[K]) => any;
}, KOn extends keyof TRes = keyof TRes> implements PromiseLike<RHandler<THdl>> {
private handlers;
private promise;
constructor(resPromise: Promise<AxiosResponse>, stps: {
[K in keyof TRes]: (data: any) => TRes[K];
}, handlers: THdl);
static init<TRes, KRsv extends keyof TRes>(res: Promise<AxiosResponse>, stps: {
[K in keyof TRes]: (data: any) => TRes[K];
}, kRsvs: KRsv[]): APIPromise<TRes, KRsv, {
[K in KRsv]: (data: TRes[K]) => TRes[K];
}>;
on<KK extends KOn, URst>(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<THdl[K]> : never;
}, Exclude<KOn, KK>>;
then<RRsv = never, RRjt = never>(onRsv?: (value: RHandler<THdl>) => RRsv | PromiseLike<RRsv>, onRjt?: (reason: any) => RRjt | PromiseLike<RRjt>): Promise<RRsv | RRjt>;
catch<RRjt>(onRjt: (reason: any) => RRjt | PromiseLike<RRjt>): Promise<RRjt>;
}
export {};

View file

@ -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;

View file

@ -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);

View file

@ -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,

View file

@ -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;

View file

@ -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<T> {', 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<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
}
// TAPI END
cp.writeln('}', -1);
// export IServerAPI
cp.writeln('');
cp.writeln('type ValueOf<T> = T[keyof T];');
cp.writeln('type Dict<T> = {[_: string]: T};');
cp.writeln('type RServerAPI<T> = 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<RServerAPI<TAPI[K]['res']>>}`, -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<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);
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<T> = {[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<never>($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<TAPI['${funcName}']['res']>,`);
// 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<keyof ${typeName}> = [`, 1);
cp.writeln(`fields: [`, 1);
cp.writeln(propTypes.map(e => `'${e[0]}',`).join(' '));
cp.writeln(']', -1);
// end of class
cp.writeln(`] as Array<keyof ${typeName}>`, -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;

View file

@ -1,43 +1,89 @@
import {AxiosResponse} from 'axios';
class BadResponseError extends Error {
constructor(public err: Error, public res: AxiosResponse<any>) {
super(err.toString());
type ValueOf<T> = T[keyof T];
type RHandler<T> = ValueOf<{[K in keyof T]:
T[K] extends (data: any) => infer U ? U : never}>;
function typeGuard<T extends U, U=any>(checker: (x: U) => boolean) {
return function(x: U): x is T {
return checker(x);
};
}
export class BadResponseError extends Error {
constructor(public res: AxiosResponse<any>, 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> = T | undefined | null;
type TPromiseOn<T, R> = Optional<(_: T) => R | PromiseLike<R>>;
export abstract class APIPromise<T> implements PromiseLike<T> {
promise: Promise<T>;
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<RHandler<THdl>> {
private promise: Promise<RHandler<THdl>>
constructor(req: Promise<AxiosResponse<any>>) {
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<AxiosResponse>,
stps: {[K in keyof TRes]: (data: any) => TRes[K]},
private handlers: THdl,
) {
this.promise = resPromise.then(res => {
const {status, data} = res;
if (!typeGuard<keyof TRes>(x=>stps.hasOwnProperty(x))(status)) {
// unexpected status
throw new BadResponseError(res, 'Unexpected');
}
const r = stps[status](data);
if (!typeGuard<KRsv>(x=>this.handlers.hasOwnProperty(x))(status)) {
// unhandled status
throw new BadResponseError(res, 'Unhandled');
}
const handler = this.handlers[status];
return handler(r);
});
}
then<T1=T, T2=never>(onRsv?: TPromiseOn<T, T1>, onRjt?: TPromiseOn<any, T2>) {
return this.promise.then(onRsv, onRjt);
}
catch<T2>(onRjt: TPromiseOn<any, T2>) {
return this.then(undefined, onRjt);
static init<TRes, KRsv extends keyof TRes>(
res: Promise<AxiosResponse>,
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<any>): T;
onSuccess<U, V>(f: Optional<(x: U)=>V>, v: U): U | V {
if (f) return f(v);
else return v;
on<KK extends KOn, URst>(
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<THdl[K]>: never},
Exclude<KOn, KK>
> {
const self = this as any;
self.handlers[status] = handler;
return self;
}
onFail<U, V>(f: Optional<(x: U)=>V>, v: U) {
if (f) return f(v);
else throw new Error();
then<RRsv=never, RRjt=never>(
onRsv?: (value: RHandler<THdl>) => RRsv|PromiseLike<RRsv>,
onRjt?: (reason: any) => RRjt|PromiseLike<RRjt>,
): Promise<RRsv|RRjt> {
return this.promise.then(onRsv, onRjt);
}
catch<RRjt>(
onRjt: (reason: any) => RRjt|PromiseLike<RRjt>,
) {
return this.then(undefined, onRjt);
}
}

View file

@ -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",