diff --git a/README.md b/README.md index 74602e7..0e6918a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,490 @@ # OpenAPI codegen for TypeScript -## TODO -- Usage -- Features -- Limitations -- Examples + +## 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 [-o ] +``` +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