[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:
parent
c482e91cbc
commit
d76000a1e4
16 changed files with 429 additions and 485 deletions
107
README.md
107
README.md
|
@ -1,12 +1,10 @@
|
|||
# OpenAPI codegen for TypeScript
|
||||
|
||||
## What is this?
|
||||
This is a TypeScript code generator which generates TypeScript classes and interfaces base on your [OpenAPI document](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md), including
|
||||
This is a TypeScript code generator which generates TypeScript types and interfaces base on your [OpenAPI document](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md), including
|
||||
- `schemas`
|
||||
- `IHandler`, type-defined interfaces for both server and client api
|
||||
- `IServerAPI`, interface for server api
|
||||
- `IClientAPI`, interface for client api
|
||||
- `apiRouter`, server api prototype using [koa router](https://github.com/koajs/router)
|
||||
- `IHandler`, types and interfaces for both server and client api
|
||||
- `apiRouter`, server api partial implementation using [koa router](https://github.com/koajs/router)
|
||||
- `ClientAPI`, client api implementation using [axios](https://github.com/axios/axios)
|
||||
|
||||
This tool assumes you use **koa router** for server and **axios** for client.
|
||||
|
@ -59,7 +57,7 @@ module.exports = {
|
|||
```
|
||||
### 3. Run this tool
|
||||
```
|
||||
yarn run api-codegen <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
|
||||
|
|
|
@ -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,
|
||||
|
|
2
dist/CodePrinter.d.ts
vendored
2
dist/CodePrinter.d.ts
vendored
|
@ -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
9
dist/CodePrinter.js
vendored
|
@ -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
4
dist/Config.d.ts
vendored
|
@ -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
4
dist/Config.js
vendored
|
@ -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
6
dist/OpenAPI.js
vendored
|
@ -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
286
dist/codegen.js
vendored
|
@ -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;
|
||||
|
|
36
dist/utils/APIPromise.d.ts
vendored
36
dist/utils/APIPromise.d.ts
vendored
|
@ -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 {};
|
||||
|
|
61
dist/utils/APIPromise.js
vendored
61
dist/utils/APIPromise.js
vendored
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
272
lib/codegen.ts
272
lib/codegen.ts
|
@ -7,50 +7,34 @@ import {
|
|||
} from './OpenAPI';
|
||||
import {CodePrinter} from './CodePrinter';
|
||||
|
||||
function codegenIServerAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) {
|
||||
const {apiDirTSPath, IHandlerName} = config;
|
||||
// import
|
||||
cp.writeln(`import * as IHandler from '${apiDirTSPath}/${IHandlerName}'`);
|
||||
// export default
|
||||
cp.writeln('\nexport default interface IAPI {', 1);
|
||||
for (const funcName of Object.keys(funcs)) {
|
||||
cp.writeln(
|
||||
`${funcName}: IHandler.${funcName}.IServerHandler;`,
|
||||
);
|
||||
}
|
||||
cp.writeln('};', -1);
|
||||
return cp.end();
|
||||
}
|
||||
|
||||
function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) {
|
||||
const {
|
||||
apiDirTSPath, schemasName, utilsTSPath,
|
||||
responsePrefix, validateStatus, stateTSPath,
|
||||
schemasName, utilsTSPath, stateTSPath,
|
||||
} = config;
|
||||
// import
|
||||
cp.writeln(`import * as Schemas from '${apiDirTSPath}/${schemasName}'`);
|
||||
cp.writeln(`import * as Schemas from './${schemasName}'`);
|
||||
cp.writeln('import {FullDate, StrictTypeParser as STP, APIPromise} ' +
|
||||
`from '${utilsTSPath}'`);
|
||||
cp.writeln('import {RouterContext as Context} from \'@koa/router\'');
|
||||
cp.writeln('import {RouterContext as CTX} from \'@koa/router\'');
|
||||
cp.writeln('import {AxiosResponse} from \'axios\'');
|
||||
cp.writeln(stateTSPath ?
|
||||
`import IState from '${stateTSPath}'` : 'type IState = any');
|
||||
// handler types
|
||||
// api req, res types
|
||||
cp.writeln(`export type TAPI = {`, 1);
|
||||
for (const [funcName, func] of Object.entries(funcs)) {
|
||||
const {reqTypes, resTypes, method} = func;
|
||||
cp.writeln(`export namespace ${funcName} {`, 1);
|
||||
cp.writeln(`${funcName}: {`, 1);
|
||||
// req
|
||||
const sReqTypes: string[] = [];
|
||||
// paras
|
||||
// req.path, ...
|
||||
cp.writeln(`req: {`, 1);
|
||||
for (const _in of ELParameterIn) {
|
||||
const paras = reqTypes[_in];
|
||||
if (paras == null) continue;
|
||||
cp.writeln(`export type T_${_in} = {`, 1);
|
||||
cp.writeln(`${_in}: {`, 1);
|
||||
for (const [propName, schemaType] of Object.entries(paras)) {
|
||||
cp.writeln(schemaType.forProp(propName)+';');
|
||||
}
|
||||
cp.writeln('};', -1);
|
||||
sReqTypes.push(`${_in}: T_${_in}`);
|
||||
}
|
||||
// body
|
||||
const {body} = reqTypes;
|
||||
|
@ -58,75 +42,40 @@ function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) {
|
|||
// PATCH's req body: Partial
|
||||
let {typeName} = body;
|
||||
if (method == 'patch') typeName = `Partial<${typeName}>`;
|
||||
cp.writeln(`export type T_body = ${typeName};`);
|
||||
sReqTypes.push(`body${body.required ? '' : '?'}: T_body`);
|
||||
cp.writeln(`body${body.required ? '' : '?'}: ${typeName};`);
|
||||
}
|
||||
// IRequest
|
||||
if (sReqTypes.length > 0) {
|
||||
cp.writeln('interface IRequest {', 1);
|
||||
for (const sReqType of sReqTypes) cp.writeln(`${sReqType};`);
|
||||
cp.writeln('}', -1);
|
||||
} else cp.writeln('interface IRequest {}');
|
||||
cp.writeln('}', -1); // req END
|
||||
// res
|
||||
cp.writeln('interface IResponses<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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Reference in a new issue