[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
|
# 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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
2
dist/CodePrinter.d.ts
vendored
2
dist/CodePrinter.d.ts
vendored
|
@ -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
9
dist/CodePrinter.js
vendored
|
@ -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
4
dist/Config.d.ts
vendored
|
@ -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
4
dist/Config.js
vendored
|
@ -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
6
dist/OpenAPI.js
vendored
|
@ -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
286
dist/codegen.js
vendored
|
@ -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>, ' +
|
|
||||||
'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();
|
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 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();
|
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;
|
||||||
|
|
36
dist/utils/APIPromise.d.ts
vendored
36
dist/utils/APIPromise.d.ts
vendored
|
@ -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 {};
|
||||||
|
|
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 });
|
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
|
||||||
catch (err) {
|
throw new BadResponseError(res, 'Unexpected');
|
||||||
rjt(new BadResponseError(err, res));
|
}
|
||||||
}
|
var r = stps[status](data);
|
||||||
}).catch(function (err) { return rjt(err); });
|
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) {
|
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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
272
lib/codegen.ts
272
lib/codegen.ts
|
@ -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>, ' +
|
|
||||||
'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();
|
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 TSTP<TAPI['${funcName}']['res']>,`);
|
||||||
|
// kRsv
|
||||||
|
cp.writeln(`[${
|
||||||
|
Object.keys(resTypes).filter(validateStatus).join(', ')
|
||||||
|
}]),`, -1);
|
||||||
}
|
}
|
||||||
cp.writeln('} as IAPI', -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;
|
||||||
|
|
|
@ -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)) {
|
||||||
}).catch(err=>rjt(err));
|
// 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>) {
|
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]},
|
||||||
catch<T2>(onRjt: TPromiseOn<any, T2>) {
|
kRsvs: KRsv[],
|
||||||
return this.then(undefined, onRjt);
|
): 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;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Reference in a new issue