Archived
1
0
Fork 0
This repository has been archived on 2024-02-06. You can view files and clone it, but cannot push or open issues or pull requests.
api-ts-gen/README.md
2020-04-09 20:21:58 +09:00

490 lines
14 KiB
Markdown

# OpenAPI codegen for TypeScript
## What is this?
It 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
- `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)
- `ClientAPI`, client api implementation using [axios](https://github.com/axios/axios)
This tool assume you use **koa router** for server and **axios** for client.
## How to use it?
### 0. Install this tool
```
yarn add -D https://github.com/supmiku39/api-codegen-ts
```
Also, install the dependencies that generated code will use.
```
yarn add koa @koa/router koa-body
yarn add -D @types/koa @types/koa__router axios
```
### 1. Write your OpenAPI document
You can use [Swagger Editor](https://swagger.io/tools/swagger-editor/) to edit your api document. Make sure to follow the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md).
This code generator generates code from `paths` and `components/schemas`.
:warning: only `get`, `post`, `update`, `delete`, `patch` are supported in `paths`
### 2. Set path alias
Determine the **output directory for generated code** and the **server api implementation location**, and edit your `tsconfig.json`:
```
{
// ...
"compilerOptions": {
// ...
"baseUrl": ".", // or your project root
"paths": {
// ...
"#api/*": ["path/to/your/output/directory/for/generated/code/*"],
"#ServerAPI": ["path/to/your/server/api/implementation"]
}
}
}
```
If you use webpack, you should also edit your `webpack.config.js`:
```
module.exports = {
// ...
resolve: {
// ...
alias: {
'#api': 'path/to/your/output/directory/for/generated/code',
// #ServerAPI is not required
},
},
};
```
### 3. Run this tool
```
yarn run api-codegen <your-openapi-document> [-o <output-dir>]
```
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 `api-generated/` directory, you can execute
```
# example
yarn run api-codegen api.yml -o api-generated
```
### 4. Implement server api
```
import IAPI from '#api/IServerAPI';
export default {
operationId: async (req, res, state, ctx) => {
// ...
},
// ...
} as IAPI;
```
The function name is the `operationId` defined in the api document.
There are 4 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
The `ctx` object from koa router. **Avoid to use it** unless required.
```
// Don't do this unless required
ctx.body = responseBody;
ctx.status = statusCode;
// Do this
res[statusCode](responseBody);
```
### 5. Mount the api router to server
```
import * as Koa from 'koa';
import apiRouter from '#api/apiRouter';
const app = new Koa();
// some other entry
app.use(apiRouter.prefix('your/api/prefix').routes());
// or simply app.use(apiRouter.routes());
app.listen(yourAppListenPort);
```
### 6. Use api in client
Simply import and use it! Like the server api, the function name will be the `operationId` defined in the api document, and the parameters will be the `parameters` and `requestBody` if defined.
```
api.{operationId}([path], [query], [header], [cookie], [body])
```
where the `path`, `query`, `header`, `cookie` is a object whose key is the `name` defined in `parameters` in the api document, and `body` is the `requestBody`.
:bulb: Note that if the method is `patch`, `req.body` will be `Partial`, which means that any property can be omitted.
```
import api from '#api/ClientAPI';
// import {FullDate} from 'api-codegen-ts/utils';
// import {SchemaA} from '#api/schemas';
// ...
api.operationWithPath({pathName: pathValue});
api.operationWithQueryAndBody({queryName: queryValue}, body);
api.operationWithPathAndQueryAndBody({
pathName1: pathvalue1,
pathName2: pathvalue2,
pathName3: pathvalue3,
}, {
queryName1: queryvalue1,
queryName2: queryvalue2,
}, body);
```
If you set the prefix of the api, you have to set the `$baseURL` of api.
```
api.$baseURL = 'same/as/the/prefix/in/server';
```
#### FullDate
If the format is `string` `date`, you should use `FullDate` instead of `Date`.
`FullDate` is a wrapper of `Date`, which implements `.toString()`, `.toJSON()` and `.valueOf()` to make it more convenience to convert it to String or JSON.
Import `FullDate` class from `api-codegen-ts/utils`.
```
import {FullDate} from 'api-codegen-ts/utils';
// initialization
new FullDate(new Date()); // from a Date instance
new FullDate(); // today
new FullDate('2012-03-31'); // 2012-03-31
new FullDate(2015, 5, 4); // 2015-05-04
new FullDate(1449446400000); // 2015-12-07
// getter
const d = new FullDate();
d.date // a Date instance clone
d.year // date.getUTCFullYear()
d.month // date.getUTCMonth()+1
d.day // date.getUTCDate()
d.dayOfWeek // date.getUTCDay()
// setter
d.year = 2018; // date.setUTCFullYear(2018)
d.month = 3; // date.setUTCMonth(3-1)
d.day = 5; // date.setUTCDate(5)
// method
/*
.advance(period: number): FullDate
return a new FullDate that advanced by $peroid days
*/
const d0 = new FullDate('2015-05-04');
d0.advance(217) // 2015-12-07
d0.advance(-1129) // 2012-03-31
/*
.distanceFrom(d0: FullDate): number
return the distance(unit: day) from d0
*/
d0.distanceFrom(new FullDate(2018, 3, 5)); // -1036
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.
Import the schema classes 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.patchB({id: 3}, {...}) // OK, simpler
api.patchB({id: 3}, SchemaB.Partial(...)); // Well, still OK
```
#### 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.
Every operation has a different `APIPromise` definition according to the possible response **status code**.
If there is only one `2xx` in the status code and error handling is not required, you can simply use `await` to get the response body.
```
const a: SchemaA = await api.getA(...);
```
However, if you want to handle several kinds of response, you can use `.onXXX(responseBody => handler)` where `XXX` is the status code.
```
api.getA(...)
.on200(a => { // the compiler knows that type of `a` is `SchemaA`
console.log('Get a successfully', a);
})
.on404(msg => {
console.log('Not found with msg', msg);
})
.on403(() => {
console.log('Forbidden with no msg');
});
```
## 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:
| type | format | | TypeScript |
|----|----|----|----|
|`integer`||→|`number`|
|`number`||→|`number`|
|`string`||→|`string`|
|`boolean`||→|`boolean`|
|`string`|`date`|→|`FullDate`|
|`string`|`date-time`|→|`Date`|
In addition, assigning `nullable: true` makes it nullable.
### Strict Type Parser(STP)
To ensure type safety, `STP` provides some static functions to convert `any` type to specific type.
| type | format | valid type | note |
|----|----|----|----|
|`integer`|`int32`|`number`, `string`|integer between `-2147483648` and `2147483647` only|
|`integer`|`int64`|`number`, `string`|no check for range and floating point|
|`number`||`number`, `string`|`NaN` and `Infinity` is valid|
|`string`||`string`||
|`boolean`||`boolean`, `string`||
|`string`|`date`|`FullDate`, `Date`, `number`, `string`||
|`string`|`date-time`|`Date`, `number`, `string`||
|`string`|`byte`|`string`, `Buffer`|no validation check|
|`string`|`binary`|`string`, `Buffer`|no validation check|
In addition, assigning `nullable: true` makes it nullable.
```
// integer.int32
39 // OK
'39' // OK
'1e3' // NG, no scientific notation
3.14 // NG, not a integer
2147483648 // NG, overflow
'1e100' // NG, overflow
NaN // NG, not a integer
Infinity // NG, not a integer
'pi' // NG, not a number
// integer.int64, number
39 // OK
'39' // OK
'1e3' // OK
3.14 // OK, although it is convertible to int64
2147483648 // OK
'1e100' // OK, although too big for int64
NaN // OK, although it is convertible to int64
Infinity // OK, although it is convertible to int64
'pi' // NG, not a number
// boolean
true, false // OK
'true', 'false' // OK
'T', 'F' // NG, only 'true' and 'false' are valid
0, 1 // NG, same reason
// FullDate
new FullDate() // OK
new Date() // OK
'2017-05-28' // OK
1449446400000 // OK, 2015-12-07
'today' // NG, not a valid Date
// Date
new Date() // OK
'2017-05-28' // OK
1449446400000 // OK, 2015-12-07T00:00:00.000Z
new FullDate() // NG
'today' // NG, not a valid Date
// string.byte
'5aeJ5qeY44GL44KP44GE44GE' // OK
'********' // Well, OK, although invalid for a base64 string
Buffer.from('nya') // OK, a Buffer(node.js)
0x52391207 // NG, number is invalid
new Blob([]) // NG, Blob is not supported
// string.
'e5a789e6a798e3818be3828fe38184e38184' // OK
'********' // Well, OK, although invalid for a hex string
Buffer.from('nya') // OK, a Buffer(node.js)
0x52391207 // NG, number is invalid
new Blob([]) // NG, Blob is not supported
```
### Schema
Base on `#/components/schemas`, it generates class definitions and constructors in `schemas.ts`.
#### Class Definition
For example,
```
Post:
type: "object"
properties:
id:
type: "integer"
format: "int32"
ts:
type: "string"
format: "date-time"
authorID
type: "integer"
format: "int32"
content:
type: "string"
nullable: true
images:
type: "array"
items:
type: "string"
format: "binary"
pinned:
type: "boolean"
```
will become
```
class Post {
id: number;
ts: string;
authorID: number;
content: string | null;
images: string[];
pinned: boolean;
}
```
#### Constructor
It also generates constructors with **strict type checking**.
The constructor 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,
```
NamedCircle:
type: "object"
properties:
name:
type: "string"
radius:
type: "number"
format: "double"
color:
type: "string"
nullable: true
```
will become
```
class NamedCircle {
name: string;
radius: number;
color: string | null;
}
```
Here are some examples for strict type checking:
```
new NamedCircle({
name: 'red circle',
radius: 39,
color: 'red',
}); // OK
new NamedCircle({
name: 'circle with null color',
radius: 0,
color: null,
}); // OK, color is nullable
new NamedCircle({
name: 'circle with null color',
radius: 0,
color: undefined,
}); // Error! color should be a number or null
new NamedCircle({
name: 'circle without given color',
radius: 0,
}); // Error! color should be given
new NamedCircle({
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.
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`.
Here are some examples.
```
NamedCircle.Partial({
name: 'circle with radius 39',
radius: 39,
color: 'cyan',
}); // OK
NamedCircle.Partial({
name: 'circle without radius and color',
}); // OK
NamedCircle.Partial({
radius: 1207,
}); // OK
NamedCircle.Partial({}); // OK
NamedCircle.Partial({
radius: 1207,
color: null,
}); // OK, color can be null
NamedCircle.Partial({
name: undefined,
radius: 1207,
color: null,
}); // OK, just skip setting name
NamedCircle.Partial({
name: null,
radius: 'miku',
color: 'cyan',
}); // Error! name is not nullable
NamedCircle.Partial({
radius: 'miku',
color: 'cyan',
}); // 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.
## Limitations
### application/json only
This tool only supports `application/json` type for request and response body. Any other type like `multipart/form` or `image/*` are **not supported** and will be ignored.
### schema $ref only
Other $ref like requestBody, responseBody are not supported currently.
## Versions
#### 1.1.1
- implement FullDate#distanceFrom(d0)
- fix FullDate timezone bug, use UTC instead
#### 1.1.0
- fix Partial constructor, enhance error msg
- add int32 STP
#### 1.0.1
- implement FullDate getter, setter, function(advance)
#### 1.0.0
- ClientAPI, ServerAPI interface, Schema TS codegen implemented
- application/json only
- get, post, put, delete, patch only