JavaScript 中基于 swagger-decorator 的自動(dòng)實(shí)體類(lèi)構(gòu)建與 Swagger 接口文檔生成
JavaScript 中基于 swagger-decorator 的自動(dòng)實(shí)體類(lèi)構(gòu)建與 Swagger 接口文檔生成是筆者對(duì)于開(kāi)源項(xiàng)目 swagger-decorator 的描述,對(duì)于不反感使用注解的項(xiàng)目中利用 swagger-decorator 添加合適的實(shí)體類(lèi)或者接口類(lèi)注解,從而實(shí)現(xiàn)支持嵌套地實(shí)體類(lèi)校驗(yàn)與生成、Sequelize 等 ORM 模型生成、基于 Swagger 的接口文檔生成等等功能。如果有對(duì) JavaScript 語(yǔ)法使用尚存不明的可以參考 JavaScript 學(xué)習(xí)與實(shí)踐資料索引或者現(xiàn)代 JavaScript 開(kāi)發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧系列文章。
swagger-decorator: 一處注解,多處使用
swagger-decorator 的初衷是為了簡(jiǎn)化 JavaScript 應(yīng)用開(kāi)發(fā),筆者在編寫(xiě) JavaScript 應(yīng)用(Web 前端 & Node.js)時(shí)發(fā)現(xiàn)我們經(jīng)常需要重復(fù)地創(chuàng)建實(shí)體類(lèi)、添加注釋或者進(jìn)行類(lèi)型校驗(yàn),swagger-decorator 希望能夠讓開(kāi)發(fā)者一處注解、多處使用。需要強(qiáng)調(diào)的是,在筆者多年的 Java 應(yīng)用開(kāi)發(fā)中也感受到,過(guò)多過(guò)度的注解反而會(huì)大大削弱代碼的可讀性,因此筆者也建議應(yīng)該在合適的時(shí)候舒心地使用 swagger-decorator,而不是本末倒置,一味地追求注解覆蓋率。swagger-decorator 已經(jīng)可以用于實(shí)體類(lèi)生成與校驗(yàn)、Sequelize ORM 實(shí)體類(lèi)生成、面向 Koa 的路由注解與 Swagger 文檔自動(dòng)生成。我們可以使用 yarn 或者 npm 安裝 swagger-decorator 依賴(lài),需要注意的是,因?yàn)槲覀冊(cè)陂_(kāi)發(fā)中還會(huì)用到注解語(yǔ)法,因此還需要添加 babel-plugin-transform-decorators-legacy 插件以進(jìn)行語(yǔ)法兼容轉(zhuǎn)化。
- # 使用 npm 安裝依賴(lài)
- $ npm install swagger-decorator -S
- $
- # 使用 yarn 安裝依賴(lài)
- $ yarn add swagger-decorator
- $ yarn add babel-plugin-transform-decorators-legacy -D
- # 導(dǎo)入需要的工具函數(shù)
- import {
- wrappingKoaRouter,
- entityProperty,
- ...
- } from "swagger-decorator";
實(shí)體類(lèi)注解
swagger-decorator 的核心 API 即是對(duì)于實(shí)體類(lèi)的注解,該注解不會(huì)改變實(shí)體類(lèi)的任何屬性表現(xiàn),只是會(huì)將注解限定的屬性特性記錄在內(nèi)置的 innerEntityObject 單例中以供后用。屬性注解 entityProperty 的方法說(shuō)明如下:
- /**
- * Description 創(chuàng)建某個(gè)屬性的描述
- * @param type 基礎(chǔ)類(lèi)型 self - 表示為自身
- * @param description 描述
- * @param required 是否為必要參數(shù)
- * @param defaultValue 默認(rèn)值
- * @param pattern
- * @param primaryKey 是否為主鍵
- * @returns {Function}
- */
- export function entityProperty({
- // 生成接口文檔需要的參數(shù)
- type = "string",
- description = "",
- required = false,
- defaultValue = undefined,
- // 進(jìn)行校驗(yàn)所需要的參數(shù)
- pattern = undefined,
- // 進(jìn)行數(shù)據(jù)庫(kù)連接需要的參數(shù)
- primaryKey = false
- }) {}
簡(jiǎn)單的用戶(hù)實(shí)體類(lèi)注解如下,這里的數(shù)據(jù)類(lèi)型 type 支持 Swagger 默認(rèn)的字符格式的類(lèi)型描述,也支持直接使用 JavaScript 類(lèi)名或者 JavaScript 數(shù)組。
- // @flow
- import { entityProperty } from "../../src/entity/decorator";
- import UserProperty from "./UserProperty";
- /**
- * Description 用戶(hù)實(shí)體類(lèi)
- */
- export default class User {
- // 編號(hào)
- @entityProperty({
- type: "integer",
- description: "user id, auto-generated",
- required: true
- })
- id: string = 0;
- // 姓名
- @entityProperty({
- type: "string",
- description: "user name, 3~12 characters",
- required: false
- })
- name: string = "name";
- // 郵箱
- @entityProperty({
- type: "string",
- description: "user email",
- pattern: "email",
- required: false
- })
- email: string = "email";
- // 屬性
- @entityProperty({
- type: UserProperty,
- description: "user property",
- required: false
- })
- property: UserProperty = new UserProperty();
- }
- export default class UserProperty {
- // 朋友列表
- @entityProperty({
- type: ["number"],
- description: "user friends, which is user ids",
- required: false
- })
- friends: [number];
- }
Swagger 內(nèi)置數(shù)據(jù)類(lèi)型定義:
Common NametypeformatCommentsintegerintegerint32signed 32 bitslongintegerint64signed 64 bitsfloatnumberfloatdoublenumberdoublestringstringbytestringbytebase64 encoded charactersbinarystringbinaryany sequence of octetsbooleanbooleandatestringdateAs defined by full-date - RFC3339dateTimestringdate-timeAs defined by date-time - RFC3339passwordstringpasswordUsed to hint UIs the input needs to be obscured.
實(shí)例生成與校驗(yàn)
實(shí)體類(lèi)定義完畢之后,我們首先可以使用 instantiate 函數(shù)為實(shí)體類(lèi)生成實(shí)例;不同于直接使用 new 關(guān)鍵字創(chuàng)建,instantiate 能夠根據(jù)指定屬性的數(shù)據(jù)類(lèi)型或者格式進(jìn)行校驗(yàn),同時(shí)能夠迭代生成嵌套地子對(duì)象。
- /**
- * Description 從實(shí)體類(lèi)中生成對(duì)象,并且進(jìn)行數(shù)據(jù)校驗(yàn);注意,這里會(huì)進(jìn)行遞歸生成,即對(duì)實(shí)體類(lèi)對(duì)象同樣進(jìn)行生成
- * @param EntityClass 實(shí)體類(lèi)
- * @param data 數(shù)據(jù)對(duì)象
- * @param ignore 是否忽略校驗(yàn)
- * @param strict 是否忽略非預(yù)定義類(lèi)屬性
- * @throws 當(dāng)校驗(yàn)失敗,會(huì)拋出異常
- */
- export function instantiate(
- EntityClass: Function,
- data: {
- [string]: any
- },
- { ignore = false, strict = true }: { ignore: boolean, strict: boolean } = {}
- ): Object {}
這里為了方便描述使用 Jest 測(cè)試用例說(shuō)明不同的使用場(chǎng)景:
- /**
- * Description 從實(shí)體類(lèi)中生成對(duì)象,并且進(jìn)行數(shù)據(jù)校驗(yàn);注意,這里會(huì)進(jìn)行遞歸生成,即對(duì)實(shí)體類(lèi)對(duì)象同樣進(jìn)行生成
- * @param EntityClass 實(shí)體類(lèi)
- * @param data 數(shù)據(jù)對(duì)象
- * @param ignore 是否忽略校驗(yàn)
- * @param strict 是否忽略非預(yù)定義類(lèi)屬性
- * @throws 當(dāng)校驗(yàn)失敗,會(huì)拋出異常
- */
- export function instantiate(
- EntityClass: Function,
- data: {
- [string]: any
- },
- { ignore = false, strict = true }: { ignore: boolean, strict: boolean } = {}
- ): Object {}
- 這里為了方便描述使用 Jest 測(cè)試用例說(shuō)明不同的使用場(chǎng)景:
- describe("測(cè)試實(shí)體類(lèi)實(shí)例化函數(shù)", () => {
- test("測(cè)試 User 類(lèi)實(shí)例化校驗(yàn)", () => {
- expect(() => {
- instantiate(User, {
- name: "name"
- }).toThrowError(/validate fail!/);
- });
- let user = instantiate(User, {
- id: 0,
- name: "name",
- email: "a@q.com"
- });
- // 判斷是否為 User 實(shí)例
- expect(user).toBeInstanceOf(User);
- });
- test("測(cè)試 ignore 參數(shù)可以允許忽略校驗(yàn)", () => {
- instantiate(
- User,
- {
- name: "name"
- },
- {
- ignore: true
- }
- );
- });
- test("測(cè)試 strict 參數(shù)可以控制是否忽略額外參數(shù)", () => {
- let user = instantiate(
- User,
- {
- name: "name",
- external: "external"
- },
- {
- ignore: true,
- strict: true
- }
- );
- expect(user).not.toHaveProperty("external", "external");
- user = instantiate(
- User,
- {
- name: "name",
- external: "external"
- },
- {
- ignore: true,
- strict: false
- }
- );
- expect(user).toHaveProperty("external", "external");
- });
- });
- describe("測(cè)試嵌套實(shí)例生成", () => {
- test("測(cè)試可以遞歸生成嵌套實(shí)體類(lèi)", () => {
- let user = instantiate(User, {
- id: 0,
- property: {
- friends: [0]
- }
- });
- expect(user.property).toBeInstanceOf(UserProperty);
- });
- });
Sequelize 模型生成
Sequelize 是 Node.js 應(yīng)用中常用的 ORM 框架,swagger-decorator 提供了 generateSequelizeModel 函數(shù)以方便從實(shí)體類(lèi)中利用現(xiàn)有的信息生成 Sequelize 對(duì)象模型;generateSequelizeModel 的第一個(gè)參數(shù)輸入實(shí)體類(lèi),第二個(gè)參數(shù)輸入需要覆寫(xiě)的模型屬性,第三個(gè)參數(shù)設(shè)置額外屬性,譬如是否需要將駝峰命名轉(zhuǎn)化為下劃線命名等等。
- const originUserSequelizeModel = generateSequelizeModel(
- User,
- {
- _id: {
- primaryKey: true
- }
- },
- {
- mappingCamelCaseToUnderScore: true
- }
- );
- const UserSequelizeModel = sequelize.define(
- "b_user",
- originUserSequelizeModel,
- {
- timestamps: false,
- underscored: true,
- freezeTableName: true
- }
- );
- UserSequelizeModel.findAll({
- attributes: { exclude: [] }
- }).then(users => {
- console.log(users);
- });
從 Flow 類(lèi)型聲明中自動(dòng)生成注解
筆者習(xí)慣使用 Flow 作為 JavaScript 的靜態(tài)類(lèi)型檢測(cè)工具,因此筆者添加了 flowToDecorator 函數(shù)以自動(dòng)地從 Flow 聲明的類(lèi)文件中提取出類(lèi)型信息;內(nèi)部原理參考現(xiàn)代 JavaScript 開(kāi)發(fā):語(yǔ)法基礎(chǔ)與實(shí)踐技巧 一書(shū)中的 JavaScript 語(yǔ)法樹(shù)與代碼轉(zhuǎn)化章節(jié)。該函數(shù)的使用方式為:
- // @flow
- import { flowToDecorator } from '../../../../src/transform/entity/flow/flow';
- test('測(cè)試從 Flow 中提取出數(shù)據(jù)類(lèi)型并且轉(zhuǎn)化為 Swagger 接口類(lèi)', () => {
- flowToDecorator('./TestEntity.js', './TestEntity.transformed.js').then(
- codeStr => {
- console.log(codeStr);
- },
- err => {
- console.error(err);
- }
- );
- });
這里對(duì)應(yīng)的 TestEntity 為:
- // @flow
- import AnotherEntity from "./AnotherEntity";
- class Entity {
- // Comment
- stringProperty: string = 0;
- classProperty: Entity = null;
- rawProperty;
- @entityProperty({
- type: "string",
- description: "this is property description",
- required: true
- })
- decoratedProperty;
- }
轉(zhuǎn)化后的實(shí)體類(lèi)為:
- // @flow
- import { entityProperty } from 'swagger-decorator';
- import AnotherEntity from './AnotherEntity';
- class Entity {
- // Comment
- @entityProperty({
- type: 'string',
- required: false,
- description: 'Comment'
- })
- stringProperty: string = 0;
- @entityProperty({
- type: Entity,
- required: false
- })
- classProperty: Entity = null;
- @entityProperty({
- type: 'string',
- required: false
- })
- rawProperty;
- @entityProperty({
- type: 'string',
- description: 'this is property description',
- required: true
- })
- decoratedProperty;
- }
接口注解與 Swagger 文檔生成
對(duì)于 Swagger 文檔規(guī)范可以參考 OpenAPI Specification ,而對(duì)于 swagger-decorator 的實(shí)際使用可以參考本項(xiàng)目的使用示例或者 基于 Koa2 的 Node.js 應(yīng)用模板 。
封裝路由對(duì)象
- import { wrappingKoaRouter } from "swagger-decorator";
- ...
- const Router = require("koa-router");
- const router = new Router();
- wrappingKoaRouter(router, "localhost:8080", "/api", {
- title: "Node Server Boilerplate",
- version: "0.0.1",
- description: "Koa2, koa-router,Webpack"
- });
- // define default route
- router.get("/", async function(ctx, next) {
- ctx.body = { msg: "Node Server Boilerplate" };
- });
- // use scan to auto add method in class
- router.scan(UserController);
定義接口類(lèi)
- export default class UserController extends UserControllerDoc {
- @apiRequestMapping("get", "/users")
- @apiDescription("get all users list")
- static async getUsers(ctx, next): [User] {
- ctx.body = [new User()];
- }
- @apiRequestMapping("get", "/user/:id")
- @apiDescription("get user object by id, only access self or friends")
- static async getUserByID(ctx, next): User {
- ctx.body = new User();
- }
- @apiRequestMapping("post", "/user")
- @apiDescription("create new user")
- static async postUser(): number {
- ctx.body = {
- statusCode: 200
- };
- }
- }
在 UserController 中是負(fù)責(zé)具體的業(yè)務(wù)實(shí)現(xiàn),為了避免過(guò)多的注解文檔對(duì)于代碼可讀性的干擾,筆者建議是將除了路徑與描述之外的信息放置到父類(lèi)中聲明;swagger-decorator 會(huì)自動(dòng)從某個(gè)接口類(lèi)的直接父類(lèi)中提取出同名方法的描述文檔。
- export default class UserControllerDoc {
- @apiResponse(200, "get users successfully", [User])
- static async getUsers(ctx, next): [User] {}
- @pathParameter({
- name: "id",
- description: "user id",
- type: "integer",
- defaultValue: 1
- })
- @queryParameter({
- name: "tags",
- description: "user tags, for filtering users",
- required: false,
- type: "array",
- items: ["string"]
- })
- @apiResponse(200, "get user successfully", User)
- static async getUserByID(ctx, next): User {}
- @bodyParameter({
- name: "user",
- description: "the new user object, must include user name",
- required: true,
- schema: User
- })
- @apiResponse(200, "create new user successfully", {
- statusCode: 200
- })
- static async postUser(): number {}
- }
運(yùn)行應(yīng)用
- run your application and open swagger docs (PS. swagger-decorator contains Swagger UI):
- /swagger
- /swagger/api.json
【本文是51CTO專(zhuān)欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)51CTO與作者聯(lián)系】