OpenAPI 自动生成接口请求代码

7,417 阅读5分钟

OpenAPI 自动生成接口请求代码

前言

OpenAPI简介

小问题,为什么Java同事只写了一些注解,我们就能看到了swagger文档的定义了呢?答案其实很简单就是OpenAPI规范。

定义

OpenAPI规范又名Swagger规范,最早属于swagger项目的一部分,下面是swaagger对于OpenAPI的定义。

”OpenAPI 规范 (OAS) 为 RESTful API 定义了一个与语言无关的标准接口,它允许人类和计算机在不访问源代码、文档或通过网络流量检查的情况下发现和理解服务的功能。如果定义得当,消费者可以用最少的实现逻辑来理解远程服务并与之交互。“

修订历史

目前用的最多是swagger 2.0版本(以我公司为例)。后续介绍以2.0举例介绍。

版本日期笔记
3.1.02021-02-15发布 OpenAPI 规范 3.1.0
3.0.32020-02-20OpenAPI 规范 3.0.3 的补丁发布
3.0.22018-10-08OpenAPI 规范 3.0.2 的补丁发布
3.0.12017-12-06OpenAPI 规范 3.0.1 的补丁发布
3.0.02017-07-26发布 OpenAPI 规范 3.0.0
2.02014-09-08Swagger 2.0 发布
1.22014-03-14正式文件的初步发布
1.12012-08-22Swagger 1.1 的发布
1.02011-08-10Swagger 规范的第一个版本

规范(Specification)

只列举常用字段,详细字段请参考swagger.io官网

字段含义
swagger版本号
info提供关于API的元数据。如果需要的话,客户端可以使用元数据
host接口host
basePath提供API的基本路径
paths必需的。API的可用路径和操作符。对应接口路径请求方式等, 以接口path为键值
definitions一个对象,用来保存由操作生成和使用的数据类型。对应后端models
tags对应后台的Controller,会按照tag进行接口分组

通过上面的介绍我们可以得出一个狭义的通俗说法,openapi(特指swagger.json)本质就是一个用户描述接口信息的json文件。

那么swagger.json已经详细描述接口信息,并且能根据json生成swagger-ui, 那我们是不是能根据swagger.json生成我们跟接口相关的请求、响应声明的typing.d.ts呢,那进一步我们能不能根据一定的规则模板生成我们想要请求代码呢,答案是肯定的,下面章节我们详细讲述怎么通过工具库做到这些。

请求、响应声明

背景

写了挺久的Typescript代码, 除了业务组件外, 对于类型定义最为迫切的其实是接口响应数据。然而根据将swagger的数据结构搬过来转为type 或 interface的工作量是巨大且繁琐的,所以有时候就会偷懒直接使用了any类型,那这样就违背了使用typecript的初衷。所以内心有个想法就是Java的既然是强类型语言,那是否存在一种场景java数据类型跟typescript数据类型的映射关系,直接将Java的类型直接转为typescript 的数据类型呢,这一搜果然,已经有相关库实现了这个功能。openapi-typescript, 并根据这个库进一步了解了openapi的规范。

实现原理

根据上述的swagger规范,将不同字段转为interface,核心代码是transform目录,根据不同的字段进行转换,底层通过nodeType实现了swagger类型与typescript的数据类型映射关系

image.png

export function nodeType(obj: any): SchemaObjectType {
  if (!obj || typeof obj !== "object") {
    return "unknown";
  }

  if (obj.$ref) {
    return "ref";
  }

  // const
  if (obj.const) {
    return "const";
  }

  // enum
  if (Array.isArray(obj.enum) && obj.enum.length) {
    return "enum";
  }

  // Treat any node with allOf/ anyOf/ oneOf as object
  if (obj.hasOwnProperty("allOf") || obj.hasOwnProperty("anyOf") || obj.hasOwnProperty("oneOf")) {
    return "object";
  }

  // boolean
  if (obj.type === "boolean") {
    return "boolean";
  }

  // string
  if (
    obj.type === "string" ||
    obj.type === "binary" ||
    obj.type === "byte" ||
    obj.type === "date" ||
    obj.type === "dateTime" ||
    obj.type === "password"
  ) {
    return "string";
  }

  // number
  if (obj.type === "integer" || obj.type === "number" || obj.type === "float" || obj.type === "double") {
    return "number";
  }

  // array
  if (obj.type === "array" || obj.items) {
    return "array";
  }

  // object
  if (obj.type === "object" || obj.hasOwnProperty("properties") || obj.hasOwnProperty("additionalProperties")) {
    return "object";
  }

  // return unknown by default
  return "unknown";
}

最终通过转换生成模板字符串,经过prettier整理格式,使用bin目录下的node脚本输入出到output文件中中。

接口请求代码

背景

随着自我追求的不断变高(不是,其实是更懒了),就想到另外一个问题,既然我都能生成接口的请求类型和响应类型了,而且openapi规范中还规定了host、basePath、path等这些字段存在,已经能完整的具备了一个接口请求代码所需要的必要条件,那么我们能不能工程化的程度更彻底一点呢,直接通过openapi的规范直接生成接口请求的代码呢?答案是肯定的, 本着不重复造轮子的想法,迅速google,果然有人先动手了,@umijs/openapi, 乍一看是umijs的一个子项目有配套的umi插件, 其实这个库本身是跟框架解耦的,只要你是typescript的项目,后端接口遵循openapi规范就可以引入这个库。

实现原理

@umijs/openapi 依赖了swagger2openapi用来做swagger2.0到OpenAPI 3.0的转换。

最后统一使用3.0的协议进行类型映射,转换为typescript类型。

其中service的生成逻辑由serviceGenerator.ts 实现,mock生成由mockGenerator.ts 实现。

我们重点介绍 serviceGenerator.ts的逻辑。

首先是类型映射,是类似于openapi-typescript的类型映射逻辑。

const getType = (schemaObject: SchemaObject | undefined, namespace: string = ''): string => {
  if (schemaObject === undefined || schemaObject === null) {
    return 'any';
  }
  if (typeof schemaObject !== 'object') {
    return schemaObject;
  }
  if (schemaObject.$ref) {
    return [namespace, getRefName(schemaObject)].filter((s) => s).join('.');
  }

  let { type } = schemaObject as any;

  const numberEnum = [
    'int64',
    'integer',
    'long',
    'float',
    'double',
    'number',
    'int',
    'float',
    'double',
    'int32',
    'int64',
  ];

  const dateEnum = ['Date', 'date', 'dateTime', 'date-time', 'datetime'];

  const stringEnum = ['string', 'email', 'password', 'url', 'byte', 'binary'];

  if (numberEnum.includes(schemaObject.format)) {
    type = 'number';
  }

  if (schemaObject.enum) {
    type = 'enum';
  }

  if (numberEnum.includes(type)) {
    return 'number';
  }

  if (dateEnum.includes(type)) {
    return 'Date';
  }

  if (stringEnum.includes(type)) {
    return 'string';
  }

  if (type === 'boolean') {
    return 'boolean';
  }

  if (type === 'array') {
    let { items } = schemaObject;
    if (schemaObject.schema) {
      items = schemaObject.schema.items;
    }

    if (Array.isArray(items)) {
      const arrayItemType = (items as any)
        .map((subType) => getType(subType.schema || subType, namespace))
        .toString();
      return `[${arrayItemType}]`;
    }
    const arrayType = getType(items, namespace);
    return arrayType.includes(' | ') ? `(${arrayType})[]` : `${arrayType}[]`;
  }

  if (type === 'enum') {
    return Array.isArray(schemaObject.enum)
      ? Array.from(
        new Set(
          schemaObject.enum.map((v) =>
            typeof v === 'string' ? `"${v.replace(/"/g, '"')}"` : getType(v),
          ),
        ),
      ).join(' | ')
      : 'string';
  }

  if (schemaObject.oneOf && schemaObject.oneOf.length) {
    return schemaObject.oneOf.map((item) => getType(item, namespace)).join(' | ');
  }
  if (schemaObject.allOf && schemaObject.allOf.length) {
    return `(${schemaObject.allOf.map((item) => getType(item, namespace)).join(' & ')})`;
  }
  if (schemaObject.type === 'object' || schemaObject.properties) {
    if (!Object.keys(schemaObject.properties || {}).length) {
      return 'Record<string, any>';
    }
    return `{ ${Object.keys(schemaObject.properties)
      .map((key) => {
        const required =
          'required' in (schemaObject.properties[key] || {})
            ? ((schemaObject.properties[key] || {}) as any).required
            : false;
        /** 
         * 将类型属性变为字符串,兼容错误格式如:
         * 3d_tile(数字开头)等错误命名,
         * 在后面进行格式化的时候会将正确的字符串转换为正常形式,
         * 错误的继续保留字符串。
         * */
        return `'${key}'${required ? '' : '?'}: ${getType(
          schemaObject.properties && schemaObject.properties[key],
          namespace,
        )}; `;
      })
      .join('')}}`;
  }
  return 'any';
};

其次是service 其他字段的生成逻辑

字段取值openAPi 字段
functionNameoperationIdlogListUsingPOST
pathbasePath/path/log/list
methodpaths—key-keypost
接口名称注释summary日志列表
content-typeconsumes"application/json"
parametersparameters根据in字段判断body还是formdata, required是否必填, $refs关联具体类型,有什么字段
responsesresponses一般只看200响应, 通过$ref 关联具体类型

openAPi 示例

{
"paths": {
  "/log/list": {
      "post": {
          "tags": [
              "log-controller"
          ],
          "summary": "日志列表",
          "operationId": "logListUsingPOST",
          "consumes": [
              "application/json"
          ],
          "produces": [
              "*/*"
          ],
          "parameters": [
              {
                  "in": "body",
                  "name": "log",
                  "description": "log",
                  "required": true,
                  "schema": {
                      "$ref": "#/definitions/LogRequest"
                  }
              }
          ],
          "responses": {
              "200": {
                  "description": "OK",
                  "schema": {
                      "$ref": "#/definitions/ResponsePageModel"
                  }
              },
              "201": {
                  "description": "Created"
              },
              "401": {
                  "description": "Unauthorized"
              },
              "403": {
                  "description": "Forbidden"
              },
              "404": {
                  "description": "Not Found"
              }
          },
          "deprecated": false
      }
  }
}
}

具体的实现源码就不看不了,就是把所需要的字段都按照上述规则提取出出来。

然后根据njk模板生成了文件, 类似与ejs模板,只截取一段大家参考下,详细的大家去github仓库查看

    {%- if api.params and api.hasParams %}
    params: {
      {%- for query in api.params.query %}
        {% if query.schema.default -%}
          // {{query.name | safe}} has a default value: {{ query.schema.default | safe }}
          '{{query.name | safe}}': '{{query.schema.default | safe}}',
        {%- endif -%}
      {%- endfor -%}
      ...{{ 'queryParams' if api.params and api.params.path else 'params' }},
      {%- for query in api.params.query %}
        {%- if query.isComplexType %}
          '{{query.name | safe}}': undefined,
          ...{{ 'queryParams' if api.params and api.params.path else 'params' }}['{{query.name | safe}}'],
        {%- endif %}
      {%- endfor -%}
    },
    {%- endif %}

其他类似与interface,mock 实现的方案跟service 大同小异,只是映射的字段和生成的模板不同而已, 就不一一表述了。

项目实践(以Vue3为例)

安装依赖

yarn add @umijs/openapi -D

新建一个配置文件

//根目录新建 script/config.js
module.exports = {
  openApi: [
    {
      requestLibPath: "import request from '@/utils/request'", // 想怎么引入封装请求方法
      schemaPath:'your host', // openAPI规范地址
      projectName: 'open-bff-patent', // 生成到哪个目录内
      apiPrefix: '"/open-bff-patent"',// 需不需要增加前缀
      serversPath: './src/service', // 生成代码到哪个目录
    }
  ],
};

//根目录新建 script/openapi.js
const { generateService } = require('@umijs/openapi')
const { openApi } = require('./config.js')


async function run() {
    for (let index = 0; index < openApi.length; index++) {
        await generateService(openApi[index])
    }
}

run()

新增一个script

"scripts": {
  "openapi": "node ./script/openapi.js"
}

生成代码

yarn openapi

代码使用

// 生成代码示例
/** 详情 GET /search/patentHistory/detail */
export async function detailUsingGET(
  // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
  params: API.detailUsingGETParams & {
    // header
    /** 登录token */
    accessToken?: string;
    /** 参数加密token */
    signature?: string;
    /** user-agent-web */
    'user-agent-web'?: string;
  },
  options?: { [key: string]: any },
) {
  return request<API.ResponseListModelString_>(`/open-bff-patent/search/patentHistory/detail`, {
    method: 'GET',
    headers: {},
    params: { ...params },
    ...(options || {}),
  });
}

// 业务代码使用
import { detailUsingGET } from '@/service/open-bff-patent/patentHistory';

async function deleteSomeThing() {
  await detailUsingGET({})
}

代码demo

后续有时间会写demo上传,目前都是公司内部项目,不方便上传。

demo在原文中,平台比较多,后续更新在原文中更新,其他博客平台,暂时不处理了。

原文地址

个人博客 OpenAPI 自动生成接口请求代码