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 # OpenAPI codegen for TypeScript
## What is this? ## 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` - `schemas`
- `IHandler`, type-defined interfaces for both server and client api - `IHandler`, types and interfaces for both server and client api
- `IServerAPI`, interface for server api - `apiRouter`, server api partial implementation using [koa router](https://github.com/koajs/router)
- `IClientAPI`, interface for client api
- `apiRouter`, server api prototype using [koa router](https://github.com/koajs/router)
- `ClientAPI`, client api implementation using [axios](https://github.com/axios/axios) - `ClientAPI`, client api implementation using [axios](https://github.com/axios/axios)
This tool assumes you use **koa router** for server and **axios** for client. This tool assumes you use **koa router** for server and **axios** for client.
@ -59,7 +57,7 @@ module.exports = {
``` ```
### 3. Run this tool ### 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`. 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 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 ### 4. Implement server api
``` ```
import IAPI from '#api/IServerAPI'; import {IServerAPI} from '#api/IServerAPI';
export default { 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. 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 #### req
The request parameters and body, defined in `parameters` and `requestBody`. 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`. 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`. `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 #### state
Alias to `ctx.state` Alias to `ctx.state`
#### ctx #### ctx
@ -108,6 +93,19 @@ ctx.status = statusCode;
// Do this // Do this
res[statusCode](responseBody); 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 ### 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`. :warning: `FullDate` use `month` from 1 to 12, which differs from `Date`. Also, `FullDate` use `day` and `dayOfWeek` instead of `date` and `day`.
#### Schema #### 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'; import {SchemaA, SchemaB} from '#api/schemas';
api.postA({id: 3}, {...}) // OK, simpler 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}, {...}) // OK, simpler
api.patchB({id: 3}, SchemaB.Partial(...)); // Well, still OK 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 #### 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. 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 ## Details
### Type Conversion ### 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: [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 ### Schema
Base on `#/components/schemas`, it generates class definitions and constructors in `schemas.ts`. Base on `#/components/schemas`, it generates interface definitions and constructor functions in `schemas.ts`.
#### Class Definition #### Interface Definition
For example, For example,
``` ```
Post: Post:
@ -353,7 +366,7 @@ Post:
``` ```
will become will become
``` ```
class Post { interface Post {
id: number; id: number;
ts: string; ts: string;
authorID: number; authorID: number;
@ -362,9 +375,9 @@ class Post {
pinned: boolean; pinned: boolean;
} }
``` ```
#### Constructor #### Constructor Function
It also generates constructors with **strict type checking**. It also generates constructor function `Schema.from` with **strict type checking**.
The constructor takes exactly one argument with literal object type. 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`. If any property of the argument is not convertible to expected type, it throws an `BadValueError`.
For example, For example,
@ -383,7 +396,7 @@ NamedCircle:
``` ```
will become will become
``` ```
class NamedCircle { interface NamedCircle {
name: string; name: string;
radius: number; radius: number;
color: string | null; color: string | null;
@ -391,37 +404,37 @@ class NamedCircle {
``` ```
Here are some examples for strict type checking: Here are some examples for strict type checking:
``` ```
new NamedCircle({ NamedCircle.from({
name: 'red circle', name: 'red circle',
radius: 39, radius: 39,
color: 'red', color: 'red',
}); // OK }); // OK
new NamedCircle({ NamedCircle.from({
name: 'circle with null color', name: 'circle with null color',
radius: 0, radius: 0,
color: null, color: null,
}); // OK, color is nullable }); // OK, color is nullable
new NamedCircle({ NamedCircle.from({
name: 'circle with null color', name: 'circle with null color',
radius: 0, radius: 0,
color: undefined, color: undefined,
}); // Error! color should be a number or null }); // Error! color should be a number or null
new NamedCircle({ NamedCircle.from({
name: 'circle without given color', name: 'circle without given color',
radius: 0, radius: 0,
}); // Error! color should be given }); // Error! color should be given
new NamedCircle({ NamedCircle.from({
name: 'circle with invalid radius', name: 'circle with invalid radius',
radius: 'miku', radius: 'miku',
color: 'cyan', color: 'cyan',
}); // Error! radius should be a number }); // Error! radius should be a number
``` ```
#### Partial Function #### 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. 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. 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`. 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 }); // 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 ## Limitations
### application/json only ### 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. Other $ref like requestBody, responseBody are not supported currently.
## Versions ## 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 #### 1.1.3
- expose fields of schemas to XXX.fields(static variable) - expose fields of schemas to XXX.fields(static variable)
#### 1.1.2 #### 1.1.2

View file

@ -9,6 +9,7 @@ const badArgv = (x, code=1) => {
'Usage: api-codegen <apiDocPath> [flags]', 'Usage: api-codegen <apiDocPath> [flags]',
'Flags:', 'Flags:',
' -o --outputDir: outputDir', ' -o --outputDir: outputDir',
' -s --stateTSPath: ctx.state type definition file path',
].join('\n')); ].join('\n'));
process.exit(code); process.exit(code);
}; };
@ -22,6 +23,8 @@ const argAttrs = ['apiDocPath'];
const flag2attr = { const flag2attr = {
o: 'outputDir', o: 'outputDir',
outputDir: 'outputDir', outputDir: 'outputDir',
s: 'stateTSPath',
stateTSPath: 'stateTSPath',
}; };
const requiredAttrs = [ const requiredAttrs = [
...argAttrs, ...argAttrs,

View file

@ -15,7 +15,7 @@ export declare class CodePrinter {
private indentString; private indentString;
private cIndent; private cIndent;
constructor(writeStream?: WriteStream, indentString?: string); constructor(writeStream?: WriteStream, indentString?: string);
writeln(s?: string, dIndent?: number): void; writeln(s?: string, dIndent?: number, pretab?: boolean): void;
write(s: string): void; write(s: string): void;
tab(x: number): void; tab(x: number): void;
end(): Promise<void>; end(): Promise<void>;

9
dist/CodePrinter.js vendored
View file

@ -23,14 +23,15 @@ var CodePrinter = /** @class */ (function () {
this.indentString = indentString; this.indentString = indentString;
this.cIndent = 0; this.cIndent = 0;
} }
CodePrinter.prototype.writeln = function (s, dIndent) { CodePrinter.prototype.writeln = function (s, dIndent, pretab) {
if (s === void 0) { s = ''; } if (s === void 0) { s = ''; }
if (dIndent === void 0) { dIndent = 0; } 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.cIndent = Math.max(0, this.cIndent + dIndent);
this.write(this.indentString.repeat(this.cIndent) + s + "\n"); this.write(this.indentString.repeat(this.cIndent) + s + "\n");
if (dIndent > 0) if (!pretab)
this.cIndent += dIndent; this.cIndent = Math.max(0, this.cIndent + dIndent);
}; };
CodePrinter.prototype.write = function (s) { CodePrinter.prototype.write = function (s) {
this.writeStream.write(s); this.writeStream.write(s);

4
dist/Config.d.ts vendored
View file

@ -5,14 +5,10 @@ export interface ConfigRequired {
export interface ConfigOptional { export interface ConfigOptional {
interfacePrefix: string; interfacePrefix: string;
indentString: string; indentString: string;
responsePrefix: string;
schemasName: string; schemasName: string;
IHandlerName: string; IHandlerName: string;
IServerAPIName: string;
IClientAPIName: string;
ClientAPIName: string; ClientAPIName: string;
routerName: string; routerName: string;
apiDirTSPath: string;
ServerAPITSPath: string; ServerAPITSPath: string;
utilsTSPath: string; utilsTSPath: string;
stateTSPath: string | null; stateTSPath: string | null;

4
dist/Config.js vendored
View file

@ -4,16 +4,12 @@ exports.configDefault = {
// format // format
interfacePrefix: 'I', interfacePrefix: 'I',
indentString: ' ', indentString: ' ',
responsePrefix: '',
// name // name
schemasName: 'schemas', schemasName: 'schemas',
IHandlerName: 'IHandler', IHandlerName: 'IHandler',
IServerAPIName: 'IServerAPI',
IClientAPIName: 'IClientAPI',
ClientAPIName: 'ClientAPI', ClientAPIName: 'ClientAPI',
routerName: 'apiRouter', routerName: 'apiRouter',
// TS path // TS path
apiDirTSPath: '#api',
ServerAPITSPath: '#ServerAPI', ServerAPITSPath: '#ServerAPI',
utilsTSPath: '@supmiku39/api-ts-gen/utils', utilsTSPath: '@supmiku39/api-ts-gen/utils',
stateTSPath: null, stateTSPath: null,

6
dist/OpenAPI.js vendored
View file

@ -73,7 +73,7 @@ var SchemaType = /** @class */ (function () {
SchemaType.prototype.stp = function (prop, label, partial) { SchemaType.prototype.stp = function (prop, label, partial) {
if (partial === void 0) { partial = false; } if (partial === void 0) { partial = false; }
var stp = SchemaType.gcStp(prop, this.schema, label, partial); 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) { SchemaType.typeNameOf = function (schema) {
var _a; var _a;
@ -124,9 +124,7 @@ var SchemaType = /** @class */ (function () {
// object // object
if (isReference(schema)) { if (isReference(schema)) {
var typeName = new SchemaType(schema, true).typeName; var typeName = new SchemaType(schema, true).typeName;
return partial ? return typeName + "." + (partial ? 'Partial' : 'from') + "(" + para + ")";
typeName + ".Partial(" + para + ")" :
"new " + typeName + "(" + para + ")";
} }
// any // any
var type = schema.type, nullable = schema.nullable, format = schema.format; 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 Config_1 = require("./Config");
var OpenAPI_1 = require("./OpenAPI"); var OpenAPI_1 = require("./OpenAPI");
var CodePrinter_1 = require("./CodePrinter"); 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) { 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 // import
cp.writeln("import * as Schemas from '" + apiDirTSPath + "/" + schemasName + "'"); cp.writeln("import * as Schemas from './" + schemasName + "'");
cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' + cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' +
("from '" + utilsTSPath + "'")); ("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('import {AxiosResponse} from \'axios\'');
cp.writeln(stateTSPath ? cp.writeln(stateTSPath ?
"import IState from '" + stateTSPath + "'" : 'type IState = any'); "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++) { for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) {
var _b = _a[_i], funcName = _b[0], func = _b[1]; var _b = _a[_i], funcName = _b[0], func = _b[1];
var reqTypes = func.reqTypes, resTypes = func.resTypes, method = func.method; var reqTypes = func.reqTypes, resTypes = func.resTypes, method = func.method;
cp.writeln("export namespace " + funcName + " {", 1); cp.writeln(funcName + ": {", 1);
// req // req
var sReqTypes = []; // req.path, ...
// paras cp.writeln("req: {", 1);
for (var _c = 0, ELParameterIn_1 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_1.length; _c++) { for (var _c = 0, ELParameterIn_1 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_1.length; _c++) {
var _in = ELParameterIn_1[_c]; var _in = ELParameterIn_1[_c];
var paras = reqTypes[_in]; var paras = reqTypes[_in];
if (paras == null) if (paras == null)
continue; continue;
cp.writeln("export type T_" + _in + " = {", 1); cp.writeln(_in + ": {", 1);
for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) { for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) {
var _f = _e[_d], propName = _f[0], schemaType = _f[1]; var _f = _e[_d], propName = _f[0], schemaType = _f[1];
cp.writeln(schemaType.forProp(propName) + ';'); cp.writeln(schemaType.forProp(propName) + ';');
} }
cp.writeln('};', -1); cp.writeln('};', -1);
sReqTypes.push(_in + ": T_" + _in);
} }
// body // body
var body = reqTypes.body; var body = reqTypes.body;
@ -56,76 +43,38 @@ function codegenIHandler(funcs, config, cp) {
var typeName = body.typeName; var typeName = body.typeName;
if (method == 'patch') if (method == 'patch')
typeName = "Partial<" + typeName + ">"; typeName = "Partial<" + typeName + ">";
cp.writeln("export type T_body = " + typeName + ";"); cp.writeln("body" + (body.required ? '' : '?') + ": " + typeName + ";");
sReqTypes.push("body" + (body.required ? '' : '?') + ": T_body");
} }
// IRequest cp.writeln('}', -1); // req END
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 {}');
// res // res
cp.writeln('interface IResponses<T> {', 1); cp.writeln("res: {", 1);
for (var _h = 0, _j = Object.entries(resTypes); _h < _j.length; _h++) { for (var _g = 0, _h = Object.entries(resTypes); _g < _h.length; _g++) {
var _k = _j[_h], status_1 = _k[0], schema = _k[1]; var _j = _h[_g], status_1 = _j[0], schema = _j[1];
cp.writeln("" + responsePrefix + status_1 + ": " + ("(" + schema.forProp('body') + ") => T;")); cp.writeln(schema.required ?
schema.forProp(status_1) + ";" : status_1 + ": void;");
} }
cp.writeln('}', -1); // res END
// operation END
cp.writeln('}', -1); cp.writeln('}', -1);
cp.writeln('export interface IServerHandler {', 1); }
cp.writeln('(req: IRequest, res: IResponses<void>, ' + // TAPI END
'state: IState, ctx: Context): void;');
cp.writeln('}', -1); cp.writeln('}', -1);
// class _ResponsePromise // export IServerAPI
var validTypes = new Set(); cp.writeln('');
cp.writeln('export class ResponsePromise<T> extends ' + cp.writeln('type ValueOf<T> = T[keyof T];');
'APIPromise<T|T_ValidResponse> {', 1); cp.writeln('type Dict<T> = {[_: string]: T};');
// handler cp.writeln('type RServerAPI<T> = ValueOf<', 1);
cp.writeln('private handlers: Partial<IResponses<T>> = {};'); cp.writeln('{[K in keyof T]: T[K] extends void ? [K, any?] : [K, T[K]]}>;', -1, false);
// on cp.writeln('export type IServerAPI = {[K in keyof TAPI]:', 1);
cp.writeln('on<K extends keyof IResponses<T>, U>(', 1); cp.writeln("(req: TAPI[K]['req'], state: IState, ctx: CTX) =>", 1);
cp.writeln('k: K, h: IResponses<U>[K]): ResponsePromise<T|U>'); cp.writeln("Promise<RServerAPI<TAPI[K]['res']>>}", -2, false);
cp.tab(-1); // return
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
}
return cp.end(); return cp.end();
} }
function codegenRouter(funcs, config, cp) { 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 // 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 * as Router from '@koa/router'");
cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'"); cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'");
cp.writeln("import * as bodyParser from 'koa-body'"); cp.writeln("import * as bodyParser from 'koa-body'");
@ -134,43 +83,24 @@ function codegenRouter(funcs, config, cp) {
cp.writeln("type CTX = Router.RouterContext<IState>;"); cp.writeln("type CTX = Router.RouterContext<IState>;");
// router // router
cp.writeln("\nconst router = new Router<IState>();"); cp.writeln("\nconst router = new Router<IState>();");
cp.writeln('');
// function // function
cp.writeln('function isEmpty(x: any): boolean {', 1); var gcGetParams = {
cp.writeln('if(x == null || x === \'\') return true;'); path: function (attr) { return "ctx.params['" + attr + "']"; },
cp.writeln('if(typeof x === \'object\') return Object.keys(x).length===0'); query: function (attr) { return "ctx.query['" + attr + "']"; },
cp.writeln('return false;'); header: function (attr) { return "ctx.headers['" + attr + "']"; },
cp.writeln('}', -1); cookie: function (attr) { return "ctx.cookies.get('" + attr + "')"; },
cp.writeln('function nullableParse<T>(v: any, ' + };
'p: (x: any)=>T): T | undefined {', 1);
cp.writeln('return isEmpty(v) ? undefined : p(v);');
cp.writeln('}', -1);
cp.writeln('const ctxGetParas = {', 1);
cp.writeln('path: (ctx: CTX, attr: string) => ctx.params[attr],');
cp.writeln('query: (ctx: CTX, attr: string) => ctx.query[attr],');
cp.writeln('header: (ctx: CTX, attr: string) => ctx.headers[attr],');
cp.writeln('cookie: (ctx: CTX, attr: string) => ctx.cookies.get(attr),');
cp.writeln('};', -1);
// response generator
cp.writeln('function g_res<T>(ctx: CTX, ' +
'status: number, dft: string = \'\'){', 1);
cp.writeln('return (body: T) => {', 1);
cp.writeln('ctx.status = status;');
cp.writeln('ctx.body = body ?? dft;');
cp.writeln('}', -1);
cp.writeln('}', -1);
// route // route
cp.writeln("\nimport api from '" + ServerAPITSPath + "'"); cp.writeln("\nimport api from '" + ServerAPITSPath + "'");
for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) {
var _b = _a[_i], funcName = _b[0], func = _b[1]; 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 isPartial = method === 'patch';
var statuses = Object.keys(resTypes);
// TODO escape // TODO escape
var sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a var sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a
var mid = ''; var mid = '';
if (reqTypes.body) { if (reqTypes.body) {
var maxSize = reqTypes.body.maxSize; var maxSize = reqTypes.body.maxSize; // TODO doc
var config_1 = maxSize == null ? '' : "{jsonLimit: '" + maxSize + "'}"; var config_1 = maxSize == null ? '' : "{jsonLimit: '" + maxSize + "'}";
mid = "bodyParser(" + config_1 + "), "; mid = "bodyParser(" + config_1 + "), ";
} }
@ -181,8 +111,8 @@ function codegenRouter(funcs, config, cp) {
} }
else { else {
cp.writeln('let req;'); cp.writeln('let req;');
cp.writeln('const {body: reqBody} = ctx.request;'); cp.writeln('try {', 1);
cp.writeln('try { req = {', 1); cp.writeln('req = {', 1);
// paras // paras
for (var _c = 0, ELParameterIn_2 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_2.length; _c++) { for (var _c = 0, ELParameterIn_2 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_2.length; _c++) {
var _in = ELParameterIn_2[_c]; var _in = ELParameterIn_2[_c];
@ -192,7 +122,7 @@ function codegenRouter(funcs, config, cp) {
cp.writeln(_in + ": {", 1); cp.writeln(_in + ": {", 1);
for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) { for (var _d = 0, _e = Object.entries(paras); _d < _e.length; _d++) {
var _f = _e[_d], name_1 = _f[0], schema = _f[1]; 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; var label = "req." + _in;
cp.writeln(name_1 + ": " + schema.stp(pn, label) + ","); cp.writeln(name_1 + ": " + schema.stp(pn, label) + ",");
} }
@ -201,60 +131,47 @@ function codegenRouter(funcs, config, cp) {
// body // body
var body = reqTypes.body; var body = reqTypes.body;
if (body != null) { 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.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.writeln('return ctx.throw(400, err.toString());');
cp.tab(-1); cp.tab(-1);
cp.writeln('throw err;'); cp.writeln('throw err;');
cp.writeln('}', -1); 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 // call
cp.writeln("await api." + funcName + "(req, res, ctx.state, ctx);"); cp.writeln("const r = await api." + funcName + "(req, ctx.state, ctx);");
cp.writeln('})', -1); cp.writeln("ctx.status = r[0];");
cp.writeln("ctx.body = r[1] ?? '';");
// ctx END
cp.writeln('});', -1);
} }
cp.writeln('\nexport default router;'); cp.writeln('\nexport default router;');
return cp.end(); 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) { 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 // import
cp.writeln("import * as _IAPI from '" + apiDirTSPath + "/" + IClientAPIName + "'"); cp.writeln("import {TAPI} from './" + IHandlerName + "'");
cp.writeln("import IAPI from '" + apiDirTSPath + "/" + IClientAPIName + "'"); cp.writeln("import * as Schemas from './" + schemasName + "'");
cp.writeln("import * as IHandler from '" + apiDirTSPath + "/" + IHandlerName + "'"); cp.writeln("import {APIPromise, StrictTypeParser as STP} from '" + utilsTSPath + "'");
cp.writeln('import axios from \'axios\''); 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 // axios
cp.writeln('\nconst $http = axios.create({', 1); cp.writeln('const $http = axios.create({', 1);
cp.writeln('validateStatus: ()=>true,'); cp.writeln('validateStatus: ()=>true,');
cp.writeln('});', -1); cp.writeln('});', -1);
// function // function
cp.writeln('\nfunction urlReplacer(url: string, ' + cp.writeln('\nfunction urlReplacer(url: string, ' +
'rules: {[_: string]: any}): string {', 1); 'rules: {[_: string]: any}): string {', 1);
cp.writeln('for(const [attr, value] of Object.entries(rules))', 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('return url;', -1);
cp.writeln('};', -1); cp.writeln('};', -1);
// implementation // implementation
@ -267,38 +184,36 @@ function codegenClientAPI(funcs, config, cp) {
cp.writeln('return config;'); cp.writeln('return config;');
cp.writeln('}, err => Promise.reject(err));', -1); cp.writeln('}, err => Promise.reject(err));', -1);
cp.writeln('},', -1); cp.writeln('},', -1);
// functions var _loop_1 = function (funcName, func) {
for (var _i = 0, _a = Object.entries(funcs); _i < _a.length; _i++) { var gcReq = function (_in) { return "TAPI['" + funcName + "']['req']['" + _in + "']"; };
var _b = _a[_i], funcName = _b[0], func = _b[1]; var method = func.method, url = func.url, reqTypes = func.reqTypes, resTypes = func.resTypes;
var ncHandler = "IHandler." + funcName;
var method = func.method, url = func.url, reqTypes = func.reqTypes;
var query = reqTypes.query, header = reqTypes.header, path_1 = reqTypes.path, body = reqTypes.body; // TODO cookie var query = reqTypes.query, header = reqTypes.header, path_1 = reqTypes.path, body = reqTypes.body; // TODO cookie
// name // name
cp.writeln(funcName + "(", 1); cp.writeln(funcName + ": (", 1);
// paras // paras
for (var _c = 0, ELParameterIn_3 = OpenAPI_1.ELParameterIn; _c < ELParameterIn_3.length; _c++) { for (var _i = 0, ELParameterIn_3 = OpenAPI_1.ELParameterIn; _i < ELParameterIn_3.length; _i++) {
var _in = ELParameterIn_3[_c]; var _in = ELParameterIn_3[_i];
var paras = reqTypes[_in]; var paras = reqTypes[_in];
if (paras == null) if (paras == null)
continue; continue;
var _required = false; var _required = false;
for (var _d = 0, _e = Object.values(paras); _d < _e.length; _d++) { for (var _a = 0, _b = Object.values(paras); _a < _b.length; _a++) {
var required = _e[_d].required; var required = _b[_a].required;
if (required) { if (required) {
_required = true; _required = true;
break; break;
} }
} }
cp.writeln(_in + ": " + ncHandler + ".T_" + _in + (_required ? '' : '={}') + ","); cp.writeln(_in + ": " + gcReq(_in) + (_required ? '' : '={}') + ",");
} }
// body // body
if (body != null) { 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.tab(-1);
cp.writeln("){return new " + ncHandler + cp.writeln(") => APIPromise.init($http({", 1);
'.ResponsePromise<never>($http({', 1); // req
cp.writeln("method: '" + method + "',"); cp.writeln("method: '" + method + "',");
var sURL = "'" + url + "'"; var sURL = "'" + url + "'";
cp.writeln("url: " + (path_1 ? "urlReplacer(" + sURL + ", path)" : sURL) + ","); cp.writeln("url: " + (path_1 ? "urlReplacer(" + sURL + ", path)" : sURL) + ",");
@ -308,21 +223,37 @@ function codegenClientAPI(funcs, config, cp) {
cp.writeln('header: header,'); cp.writeln('header: header,');
if (body != null) if (body != null)
cp.writeln('data: body,'); 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 IAPI', -1); 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('}');
return cp.end(); return cp.end();
} }
function codegenSchemas(schemas, config, cp) { function codegenSchemas(schemas, config, cp) {
var utilsTSPath = config.utilsTSPath; var utilsTSPath = config.utilsTSPath;
// import // import
cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'"); cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'");
cp.writeln();
// schema // schema
for (var _i = 0, _a = Object.entries(schemas); _i < _a.length; _i++) { for (var _i = 0, _a = Object.entries(schemas); _i < _a.length; _i++) {
var _b = _a[_i], typeName = _b[0], schema = _b[1]; var _b = _a[_i], typeName = _b[0], schema = _b[1];
cp.writeln();
if (OpenAPI_1.isObjectSchema(schema)) { if (OpenAPI_1.isObjectSchema(schema)) {
cp.writeln("export class " + typeName + " {", 1); // interface
cp.writeln("export interface " + typeName + " {", 1);
var propTypes = []; var propTypes = [];
for (var _c = 0, _d = Object.entries(schema.properties); _c < _d.length; _c++) { for (var _c = 0, _d = Object.entries(schema.properties); _c < _d.length; _c++) {
var _e = _d[_c], propName = _e[0], prop = _e[1]; var _e = _d[_c], propName = _e[0], prop = _e[1];
@ -330,28 +261,31 @@ function codegenSchemas(schemas, config, cp) {
propTypes.push([propName, propType]); propTypes.push([propName, propType]);
cp.writeln(propType.forProp(propName) + ';'); cp.writeln(propType.forProp(propName) + ';');
} }
// constructor cp.writeln('}', -1); // interface END
cp.writeln('constructor(o: {[_: string]: any}){', 1); // 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++) { for (var _f = 0, propTypes_1 = propTypes; _f < propTypes_1.length; _f++) {
var _g = propTypes_1[_f], n = _g[0], t = _g[1]; 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 // 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 + "> = {};"); cp.writeln("const r: Partial<" + typeName + "> = {};");
var locPartial = "Partial<" + typeName + ">"; var locPartial = "Partial<" + typeName + ">";
for (var _h = 0, propTypes_2 = propTypes; _h < propTypes_2.length; _h++) { for (var _h = 0, propTypes_2 = propTypes; _h < propTypes_2.length; _h++) {
var _j = propTypes_2[_h], n = _j[0], t = _j[1]; 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('return r;');
cp.writeln('}', -1); cp.writeln('},', -1);
// fields // 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(propTypes.map(function (e) { return "'" + e[0] + "',"; }).join(' '));
cp.writeln(']', -1); cp.writeln("] as Array<keyof " + typeName + ">", -1);
// end of class // end of const
cp.writeln('}', -1); cp.writeln('}', -1);
} }
else { else {
@ -373,10 +307,8 @@ function codegen(openAPI, configUser) {
// handler // handler
ps.push(codegenIHandler(apiFuncs, config, gCP(config.IHandlerName))); ps.push(codegenIHandler(apiFuncs, config, gCP(config.IHandlerName)));
// server // server
ps.push(codegenIServerAPI(apiFuncs, config, gCP(config.IServerAPIName)));
ps.push(codegenRouter(apiFuncs, config, gCP(config.routerName))); ps.push(codegenRouter(apiFuncs, config, gCP(config.routerName)));
// client // client
ps.push(codegenIClientAPI(apiFuncs, config, gCP(config.IClientAPIName)));
ps.push(codegenClientAPI(apiFuncs, config, gCP(config.ClientAPIName))); ps.push(codegenClientAPI(apiFuncs, config, gCP(config.ClientAPIName)));
// schema // schema
var schemas = (_a = openAPI.components) === null || _a === void 0 ? void 0 : _a.schemas; var schemas = (_a = openAPI.components) === null || _a === void 0 ? void 0 : _a.schemas;

View file

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

View file

@ -13,49 +13,60 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
function typeGuard(checker) {
return function (x) {
return checker(x);
};
}
var BadResponseError = /** @class */ (function (_super) { var BadResponseError = /** @class */ (function (_super) {
__extends(BadResponseError, _super); __extends(BadResponseError, _super);
function BadResponseError(err, res) { function BadResponseError(res, label) {
var _this = _super.call(this, err.toString()) || this; var _this = _super.call(this, label + " status code: " + res.status + "\ndata: " + (typeof res.data === 'object' ? JSON.stringify(res.data) : res.data)) || this;
_this.err = err;
_this.res = res; _this.res = res;
Object.setPrototypeOf(_this, BadResponseError.prototype); Object.setPrototypeOf(_this, BadResponseError.prototype);
return _this; return _this;
} }
return BadResponseError; return BadResponseError;
}(Error)); }(Error));
exports.BadResponseError = BadResponseError;
var APIPromise = /** @class */ (function () { var APIPromise = /** @class */ (function () {
function APIPromise(req) { function APIPromise(resPromise, stps, handlers) {
var _this = this; var _this = this;
this.promise = new Promise(function (rsv, rjt) { this.handlers = handlers;
req.then(function (res) { this.promise = resPromise.then(function (res) {
try { var status = res.status, data = res.data;
rsv(_this.onResponse(res)); if (!typeGuard(function (x) { return stps.hasOwnProperty(x); })(status)) {
// unexpected status
throw new BadResponseError(res, 'Unexpected');
} }
catch (err) { var r = stps[status](data);
rjt(new BadResponseError(err, res)); if (!typeGuard(function (x) { return _this.handlers.hasOwnProperty(x); })(status)) {
// unhandled status
throw new BadResponseError(res, 'Unhandled');
} }
}).catch(function (err) { return rjt(err); }); 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) { APIPromise.prototype.then = function (onRsv, onRjt) {
return this.promise.then(onRsv, onRjt); return this.promise.then(onRsv, onRjt);
}; };
APIPromise.prototype.catch = function (onRjt) { APIPromise.prototype.catch = function (onRjt) {
return this.then(undefined, 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; return APIPromise;
}()); }());
exports.APIPromise = APIPromise; exports.APIPromise = APIPromise;

View file

@ -22,10 +22,10 @@ export class CodePrinter {
private writeStream: WriteStream = new StringStream(), private writeStream: WriteStream = new StringStream(),
private indentString: string = ' ', private indentString: string = ' ',
) {} ) {}
writeln(s: string = '', dIndent: number = 0) { writeln(s = '', dIndent = 0, pretab = dIndent<0) {
if (dIndent < 0) this.cIndent = Math.max(0, this.cIndent + dIndent); if (pretab) this.cIndent = Math.max(0, this.cIndent + dIndent);
this.write(`${this.indentString.repeat(this.cIndent) + s}\n`); 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) { write(s: string) {
this.writeStream.write(s); this.writeStream.write(s);

View file

@ -6,16 +6,12 @@ export interface ConfigOptional {
// format // format
interfacePrefix: string; interfacePrefix: string;
indentString: string; indentString: string;
responsePrefix: string;
// name // name
schemasName: string; schemasName: string;
IHandlerName: string; IHandlerName: string;
IServerAPIName: string;
IClientAPIName: string;
ClientAPIName: string; ClientAPIName: string;
routerName: string; routerName: string;
// TS path // TS path
apiDirTSPath: string;
ServerAPITSPath: string; ServerAPITSPath: string;
utilsTSPath: string; utilsTSPath: string;
stateTSPath: string | null; stateTSPath: string | null;
@ -27,16 +23,12 @@ export const configDefault: ConfigOptional = {
// format // format
interfacePrefix: 'I', interfacePrefix: 'I',
indentString: ' ', indentString: ' ',
responsePrefix: '',
// name // name
schemasName: 'schemas', schemasName: 'schemas',
IHandlerName: 'IHandler', IHandlerName: 'IHandler',
IServerAPIName: 'IServerAPI',
IClientAPIName: 'IClientAPI',
ClientAPIName: 'ClientAPI', ClientAPIName: 'ClientAPI',
routerName: 'apiRouter', routerName: 'apiRouter',
// TS path // TS path
apiDirTSPath: '#api',
ServerAPITSPath: '#ServerAPI', ServerAPITSPath: '#ServerAPI',
utilsTSPath: '@supmiku39/api-ts-gen/utils', utilsTSPath: '@supmiku39/api-ts-gen/utils',
stateTSPath: null, stateTSPath: null,

View file

@ -147,7 +147,7 @@ export class SchemaType {
} }
stp(prop: string, label: string, partial: boolean=false): string { stp(prop: string, label: string, partial: boolean=false): string {
const stp = SchemaType.gcStp(prop, this.schema, label, partial); 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; private schema: Schema | Reference;
@ -195,9 +195,7 @@ export class SchemaType {
// object // object
if (isReference(schema)) { if (isReference(schema)) {
const typeName = new SchemaType(schema, true).typeName; const typeName = new SchemaType(schema, true).typeName;
return partial ? return `${typeName}.${partial ? 'Partial': 'from'}(${para})`;
`${typeName}.Partial(${para})` :
`new ${typeName}(${para})`;
} }
// any // any
const {type, nullable, format} = schema; const {type, nullable, format} = schema;

View file

@ -7,50 +7,34 @@ import {
} from './OpenAPI'; } from './OpenAPI';
import {CodePrinter} from './CodePrinter'; 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) { function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const { const {
apiDirTSPath, schemasName, utilsTSPath, schemasName, utilsTSPath, stateTSPath,
responsePrefix, validateStatus, stateTSPath,
} = config; } = config;
// import // import
cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`); cp.writeln(`import * as Schemas from './${schemasName}'`);
cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' + cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' +
`from '${utilsTSPath}'`); `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('import {AxiosResponse} from \'axios\'');
cp.writeln(stateTSPath ? cp.writeln(stateTSPath ?
`import IState from '${stateTSPath}'` : 'type IState = any'); `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)) { for (const [funcName, func] of Object.entries(funcs)) {
const {reqTypes, resTypes, method} = func; const {reqTypes, resTypes, method} = func;
cp.writeln(`export namespace ${funcName} {`, 1); cp.writeln(`${funcName}: {`, 1);
// req // req
const sReqTypes: string[] = []; // req.path, ...
// paras cp.writeln(`req: {`, 1);
for (const _in of ELParameterIn) { for (const _in of ELParameterIn) {
const paras = reqTypes[_in]; const paras = reqTypes[_in];
if (paras == null) continue; if (paras == null) continue;
cp.writeln(`export type T_${_in} = {`, 1); cp.writeln(`${_in}: {`, 1);
for (const [propName, schemaType] of Object.entries(paras)) { for (const [propName, schemaType] of Object.entries(paras)) {
cp.writeln(schemaType.forProp(propName)+';'); cp.writeln(schemaType.forProp(propName)+';');
} }
cp.writeln('};', -1); cp.writeln('};', -1);
sReqTypes.push(`${_in}: T_${_in}`);
} }
// body // body
const {body} = reqTypes; const {body} = reqTypes;
@ -58,75 +42,40 @@ function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) {
// PATCH's req body: Partial // PATCH's req body: Partial
let {typeName} = body; let {typeName} = body;
if (method == 'patch') typeName = `Partial<${typeName}>`; if (method == 'patch') typeName = `Partial<${typeName}>`;
cp.writeln(`export type T_body = ${typeName};`); cp.writeln(`body${body.required ? '' : '?'}: ${typeName};`);
sReqTypes.push(`body${body.required ? '' : '?'}: T_body`);
} }
// IRequest cp.writeln('}', -1); // req END
if (sReqTypes.length > 0) {
cp.writeln('interface IRequest {', 1);
for (const sReqType of sReqTypes) cp.writeln(`${sReqType};`);
cp.writeln('}', -1);
} else cp.writeln('interface IRequest {}');
// res // res
cp.writeln('interface IResponses<T> {', 1); cp.writeln(`res: {`, 1);
for (const [status, schema] of Object.entries(resTypes)) { for (const [status, schema] of Object.entries(resTypes)) {
cp.writeln(`${responsePrefix}${status}: ${ cp.writeln(schema.required ?
`(${schema.forProp('body')}) => T;` `${schema.forProp(status)};`: `${status}: void;`);
}`);
} }
cp.writeln('}', -1); // res END
// operation END
cp.writeln('}', -1); cp.writeln('}', -1);
cp.writeln('export interface IServerHandler {', 1); }
cp.writeln('(req: IRequest, res: IResponses<void>, ' + // TAPI END
'state: IState, ctx: Context): void;');
cp.writeln('}', -1); cp.writeln('}', -1);
// class _ResponsePromise // export IServerAPI
const validTypes = new Set<string>(); cp.writeln('');
cp.writeln('export class ResponsePromise<T> extends ' + cp.writeln('type ValueOf<T> = T[keyof T];');
'APIPromise<T|T_ValidResponse> {', 1); cp.writeln('type Dict<T> = {[_: string]: T};');
// handler cp.writeln('type RServerAPI<T> = ValueOf<', 1);
cp.writeln('private handlers: Partial<IResponses<T>> = {};'); cp.writeln('{[K in keyof T]: T[K] extends void ? [K, any?] : [K, T[K]]}>;',
// on -1, false);
cp.writeln('on<K extends keyof IResponses<T>, U>(', 1); cp.writeln('export type IServerAPI = {[K in keyof TAPI]:', 1);
cp.writeln('k: K, h: IResponses<U>[K]): ResponsePromise<T|U>'); cp.writeln(`(req: TAPI[K]['req'], state: IState, ctx: CTX) =>`, 1);
cp.tab(-1); cp.writeln(`Promise<RServerAPI<TAPI[K]['res']>>}`, -2, false);
cp.writeln('{ const e: ResponsePromise<T|U> = this; ' + // return
'e.handlers[k] = h; return e; }');
// onResponse
cp.writeln('onResponse(res: AxiosResponse<any>){', 1);
cp.writeln('const {status, data} = res');
cp.writeln('switch(status){', 1);
for (const [status, schema] of Object.entries(resTypes)) {
// TODO void -> string or any
const isValid = validateStatus(status);
cp.writeln(`case ${status}: return this.${
isValid ? 'onSuccess' : 'onFail'
}(this.handlers[${status}],`, 1);
cp.writeln(`${schema.stp('data', 'res.body')});`);
cp.tab(-1);
if (isValid) validTypes.add(schema.typeName);
}
cp.writeln('}', -1); // end switch
cp.writeln('throw new Error(\'Unexpect status code: \'+status);');
cp.writeln('}', -1); // end onResponse
cp.writeln('}', -1); // end class
// valid type
const sValidTypes = Array.from(validTypes.values()).join(' | ');
cp.writeln(`export type T_ValidResponse = ${sValidTypes};`);
// export client handler
cp.writeln('export interface IClientHandler {', 1);
cp.writeln(`(${sReqTypes.join(', ')}): ResponsePromise<never>;`);
cp.writeln('}', -1); // end client handler
cp.writeln('}', -1); // end namespace
}
return cp.end(); return cp.end();
} }
function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) { function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const { const {
apiDirTSPath, schemasName, responsePrefix, schemasName, ServerAPITSPath, utilsTSPath, stateTSPath,
ServerAPITSPath, utilsTSPath, stateTSPath,
} = config; } = config;
// import // 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 * as Router from '@koa/router'`);
cp.writeln( cp.writeln(
`import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`); `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>;`); cp.writeln(`type CTX = Router.RouterContext<IState>;`);
// router // router
cp.writeln(`\nconst router = new Router<IState>();`); cp.writeln(`\nconst router = new Router<IState>();`);
cp.writeln('');
// function // function
cp.writeln('function isEmpty(x: any): boolean {', 1); const gcGetParams = {
cp.writeln('if(x == null || x === \'\') return true;'); path: (attr: string) => `ctx.params['${attr}']`,
cp.writeln('if(typeof x === \'object\') return Object.keys(x).length===0'); query: (attr: string) => `ctx.query['${attr}']`,
cp.writeln('return false;'); header: (attr: string) => `ctx.headers['${attr}']`,
cp.writeln('}', -1); cookie: (attr: string) => `ctx.cookies.get('${attr}')`,
cp.writeln('function nullableParse<T>(v: any, ' + };
'p: (x: any)=>T): T | undefined {', 1);
cp.writeln('return isEmpty(v) ? undefined : p(v);');
cp.writeln('}', -1);
cp.writeln('const ctxGetParas = {', 1);
cp.writeln('path: (ctx: CTX, attr: string) => ctx.params[attr],');
cp.writeln('query: (ctx: CTX, attr: string) => ctx.query[attr],');
cp.writeln('header: (ctx: CTX, attr: string) => ctx.headers[attr],');
cp.writeln('cookie: (ctx: CTX, attr: string) => ctx.cookies.get(attr),');
cp.writeln('};', -1);
// response generator
cp.writeln('function g_res<T>(ctx: CTX, ' +
'status: number, dft: string = \'\'){', 1);
cp.writeln('return (body: T) => {', 1);
cp.writeln('ctx.status = status;');
cp.writeln('ctx.body = body ?? dft;');
cp.writeln('}', -1);
cp.writeln('}', -1);
// route // route
cp.writeln(`\nimport api from '${ServerAPITSPath}'`); cp.writeln(`\nimport api from '${ServerAPITSPath}'`);
for (const [funcName, func] of Object.entries(funcs)) { for (const [funcName, func] of Object.entries(funcs)) {
const { const {
method, url, reqTypes, resTypes, method, url, reqTypes,
} = func; } = func;
const isPartial = method === 'patch'; const isPartial = method === 'patch';
const statuses = Object.keys(resTypes);
// TODO escape // TODO escape
const sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a const sURL = url.replace(/{(.*?)}/g, ':$1'); // {a} -> :a
let mid = ''; let mid = '';
if (reqTypes.body) { if (reqTypes.body) {
const {maxSize} = reqTypes.body; const {maxSize} = reqTypes.body; // TODO doc
const config = maxSize == null ? '' : `{jsonLimit: '${maxSize}'}`; const config = maxSize == null ? '' : `{jsonLimit: '${maxSize}'}`;
mid = `bodyParser(${config}), `; mid = `bodyParser(${config}), `;
} }
@ -183,15 +113,15 @@ function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) {
cp.writeln('const req = {};'); cp.writeln('const req = {};');
} else { } else {
cp.writeln('let req;'); cp.writeln('let req;');
cp.writeln('const {body: reqBody} = ctx.request;'); cp.writeln('try {', 1);
cp.writeln('try { req = {', 1); cp.writeln('req = {', 1);
// paras // paras
for (const _in of ELParameterIn) { for (const _in of ELParameterIn) {
const paras = reqTypes[_in]; const paras = reqTypes[_in];
if (paras == null) continue; if (paras == null) continue;
cp.writeln(`${_in}: {`, 1); cp.writeln(`${_in}: {`, 1);
for (const [name, schema] of Object.entries(paras)) { for (const [name, schema] of Object.entries(paras)) {
const pn = `ctxGetParas.${_in}(ctx, '${name}')`; const pn = gcGetParams[_in](name);
const label = `req.${_in}`; const label = `req.${_in}`;
cp.writeln(`${name}: ${schema.stp(pn, label)},`); cp.writeln(`${name}: ${schema.stp(pn, label)},`);
} }
@ -200,62 +130,48 @@ function codegenRouter(funcs: APIFuncs, config: Config, cp: CodePrinter) {
// body // body
const {body} = reqTypes; const {body} = reqTypes;
if (body != null) { 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('}', -1);
cp.writeln('if(err instanceof STP.BadValueError)', 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('return ctx.throw(400, err.toString());'); cp.tab(-1);
cp.writeln('throw err;'); cp.writeln('throw err;');
cp.writeln('}', -1); 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 // call
cp.writeln(`await api.${funcName}(req, res, ctx.state, ctx);`); cp.writeln(`const r = await api.${funcName}(req, ctx.state, ctx);`);
cp.writeln('})', -1); cp.writeln(`ctx.status = r[0];`);
cp.writeln(`ctx.body = r[1] ?? '';`);
// ctx END
cp.writeln('});', -1);
} }
cp.writeln('\nexport default router;'); cp.writeln('\nexport default router;');
return cp.end(); 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) { function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
const { const {IHandlerName, schemasName, utilsTSPath, validateStatus} = config;
apiDirTSPath, IClientAPIName, IHandlerName,
} = config;
// import // import
cp.writeln(`import * as _IAPI from '${apiDirTSPath}/${IClientAPIName}'`); cp.writeln(`import {TAPI} from './${IHandlerName}'`);
cp.writeln(`import IAPI from '${apiDirTSPath}/${IClientAPIName}'`); cp.writeln(`import * as Schemas from './${schemasName}'`);
cp.writeln(`import * as IHandler from '${apiDirTSPath}/${IHandlerName}'`); cp.writeln(
cp.writeln('import axios from \'axios\''); `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 // axios
cp.writeln('\nconst $http = axios.create({', 1); cp.writeln('const $http = axios.create({', 1);
cp.writeln('validateStatus: ()=>true,'); cp.writeln('validateStatus: ()=>true,');
cp.writeln('});', -1); cp.writeln('});', -1);
// function // function
cp.writeln('\nfunction urlReplacer(url: string, ' + cp.writeln('\nfunction urlReplacer(url: string, ' +
'rules: {[_: string]: any}): string {', 1); 'rules: {[_: string]: any}): string {', 1);
cp.writeln('for(const [attr, value] of Object.entries(rules))', 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('return url;', -1);
cp.writeln('};', -1); cp.writeln('};', -1);
// implementation // implementation
@ -270,13 +186,13 @@ function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
cp.writeln('},', -1); cp.writeln('},', -1);
// functions // functions
for (const [funcName, func] of Object.entries(funcs)) { for (const [funcName, func] of Object.entries(funcs)) {
const ncHandler = `IHandler.${funcName}`; const gcReq = (_in: string) => `TAPI['${funcName}']['req']['${_in}']`;
const {method, url, reqTypes} = func; const {method, url, reqTypes, resTypes} = func;
const { const {
query, header, path, body, query, header, path, body,
} = reqTypes; // TODO cookie } = reqTypes; // TODO cookie
// name // name
cp.writeln(`${funcName}(`, 1); cp.writeln(`${funcName}: (`, 1);
// paras // paras
for (const _in of ELParameterIn) { for (const _in of ELParameterIn) {
const paras = reqTypes[_in]; const paras = reqTypes[_in];
@ -287,25 +203,35 @@ function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
_required = true; break; _required = true; break;
} }
} }
cp.writeln(`${_in}: ${ncHandler}.T_${_in}${_required ? '' : '={}'},`); cp.writeln(`${_in}: ${gcReq(_in)}${_required ? '' : '={}'},`);
} }
// body // body
if (body != null) { 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.tab(-1);
cp.writeln(`){return new ${ncHandler}`+ cp.writeln(`) => APIPromise.init($http({`, 1);
'.ResponsePromise<never>($http({', 1); // req
cp.writeln(`method: '${method}',`); cp.writeln(`method: '${method}',`);
const sURL = `'${url}'`; const sURL = `'${url}'`;
cp.writeln(`url: ${path ? `urlReplacer(${sURL}, path)` : sURL},`); cp.writeln(`url: ${path ? `urlReplacer(${sURL}, path)` : sURL},`);
if (query) cp.writeln('params: query,'); if (query) cp.writeln('params: query,');
if (header) cp.writeln('header: header,'); if (header) cp.writeln('header: header,');
if (body != null) cp.writeln('data: body,'); 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 IAPI', -1); cp.writeln(`} as TSTP<TAPI['${funcName}']['res']>,`);
// kRsv
cp.writeln(`[${
Object.keys(resTypes).filter(validateStatus).join(', ')
}]),`, -1);
}
cp.writeln('}');
return cp.end(); return cp.end();
} }
@ -314,39 +240,43 @@ function codegenSchemas(schemas: Schemas, config: Config, cp: CodePrinter) {
// import // import
cp.writeln( cp.writeln(
`import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`); `import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`);
cp.writeln();
// schema // schema
for (const [typeName, schema] of Object.entries(schemas)) { for (const [typeName, schema] of Object.entries(schemas)) {
cp.writeln();
if (isObjectSchema(schema)) { if (isObjectSchema(schema)) {
cp.writeln(`export class ${typeName} {`, 1); // interface
cp.writeln(`export interface ${typeName} {`, 1);
const propTypes: [string, SchemaType][] = []; const propTypes: [string, SchemaType][] = [];
for (const [propName, prop] of Object.entries(schema.properties)) { for (const [propName, prop] of Object.entries(schema.properties)) {
const propType = new SchemaType(prop, true); // TODO required const propType = new SchemaType(prop, true); // TODO required
propTypes.push([propName, propType]); propTypes.push([propName, propType]);
cp.writeln(propType.forProp(propName)+';'); cp.writeln(propType.forProp(propName)+';');
} }
// constructor cp.writeln('}', -1); // interface END
cp.writeln('constructor(o: {[_: string]: any}){', 1); // const
cp.writeln(`export const ${typeName} = {`, 1);
// .from
cp.writeln(`from: (o: {[_: string]: any}): ${typeName} => ({`, 1);
for (const [n, t] of propTypes) { 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 // Partial
cp.writeln( cp.writeln(
`static Partial(o: {[_: string]: any}): Partial<${typeName}> {`, 1); `Partial: (o: {[_: string]: any}): Partial<${typeName}> => {`, 1);
cp.writeln(`const r: Partial<${typeName}> = {};`); cp.writeln(`const r: Partial<${typeName}> = {};`);
const locPartial = `Partial<${typeName}>`; const locPartial = `Partial<${typeName}>`;
for (const [n, t] of propTypes) { 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)};`); t.stp(`o.${n}`, locPartial+'.'+n)};`);
} }
cp.writeln('return r;'); cp.writeln('return r;');
cp.writeln('}', -1); cp.writeln('},', -1);
// fields // fields
cp.writeln(`static fields: Array<keyof ${typeName}> = [`, 1); cp.writeln(`fields: [`, 1);
cp.writeln(propTypes.map(e => `'${e[0]}',`).join(' ')); cp.writeln(propTypes.map(e => `'${e[0]}',`).join(' '));
cp.writeln(']', -1); cp.writeln(`] as Array<keyof ${typeName}>`, -1);
// end of class // end of const
cp.writeln('}', -1); cp.writeln('}', -1);
} else { } else {
cp.writeln(`export type ${typeName} = ${SchemaType.typeNameOf(schema)}`); cp.writeln(`export type ${typeName} = ${SchemaType.typeNameOf(schema)}`);
@ -370,10 +300,8 @@ export default function codegen(openAPI: OpenAPI, configUser: ConfigUser) {
// handler // handler
ps.push(codegenIHandler(apiFuncs, config, gCP(config.IHandlerName))); ps.push(codegenIHandler(apiFuncs, config, gCP(config.IHandlerName)));
// server // server
ps.push(codegenIServerAPI(apiFuncs, config, gCP(config.IServerAPIName)));
ps.push(codegenRouter(apiFuncs, config, gCP(config.routerName))); ps.push(codegenRouter(apiFuncs, config, gCP(config.routerName)));
// client // client
ps.push(codegenIClientAPI(apiFuncs, config, gCP(config.IClientAPIName)));
ps.push(codegenClientAPI(apiFuncs, config, gCP(config.ClientAPIName))); ps.push(codegenClientAPI(apiFuncs, config, gCP(config.ClientAPIName)));
// schema // schema
const schemas = openAPI.components?.schemas; const schemas = openAPI.components?.schemas;

View file

@ -1,43 +1,89 @@
import {AxiosResponse} from 'axios'; import {AxiosResponse} from 'axios';
class BadResponseError extends Error { type ValueOf<T> = T[keyof T];
constructor(public err: Error, public res: AxiosResponse<any>) { type RHandler<T> = ValueOf<{[K in keyof T]:
super(err.toString()); 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); Object.setPrototypeOf(this, BadResponseError.prototype);
} }
} }
type Optional<T> = T | undefined | null; export class APIPromise<
type TPromiseOn<T, R> = Optional<(_: T) => R | PromiseLike<R>>; TRes,
export abstract class APIPromise<T> implements PromiseLike<T> { KRsv extends keyof TRes,
promise: Promise<T>; 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>>) { constructor(
this.promise = new Promise((rsv, rjt)=>{ resPromise: Promise<AxiosResponse>,
req.then(res=>{ stps: {[K in keyof TRes]: (data: any) => TRes[K]},
try { private handlers: THdl,
rsv(this.onResponse(res)); ) {
} catch (err) { this.promise = resPromise.then(res => {
rjt(new BadResponseError(err, res)); const {status, data} = res;
if (!typeGuard<keyof TRes>(x=>stps.hasOwnProperty(x))(status)) {
// unexpected status
throw new BadResponseError(res, 'Unexpected');
} }
}).catch(err=>rjt(err)); 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>) { static init<TRes, KRsv extends keyof TRes>(
return this.promise.then(onRsv, onRjt); 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;
} }
catch<T2>(onRjt: TPromiseOn<any, T2>) { return new APIPromise(res, stps, handlers);
return this.then(undefined, onRjt);
} }
abstract onResponse(res: AxiosResponse<any>): T; on<KK extends KOn, URst>(
onSuccess<U, V>(f: Optional<(x: U)=>V>, v: U): U | V { status: KK, handler: (data: TRes[KK]) => URst,
if (f) return f(v); ): APIPromise<
else return v; 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); then<RRsv=never, RRjt=never>(
else throw new Error(); 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", "name": "@supmiku39/api-ts-gen",
"version": "1.1.3", "version": "2.0.0",
"description": "OpenAPI code generator for TypeScript", "description": "OpenAPI code generator for TypeScript",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",