OpenAPI 自动生成接口请求代码
前言
OpenAPI简介
小问题,为什么Java同事只写了一些注解,我们就能看到了swagger文档的定义了呢?答案其实很简单就是OpenAPI规范。
定义
OpenAPI规范又名Swagger规范,最早属于swagger项目的一部分,下面是swaagger对于OpenAPI的定义。
”OpenAPI 规范 (OAS) 为 RESTful API 定义了一个与语言无关的标准接口,它允许人类和计算机在不访问源代码、文档或通过网络流量检查的情况下发现和理解服务的功能。如果定义得当,消费者可以用最少的实现逻辑来理解远程服务并与之交互。“
修订历史
目前用的最多是swagger 2.0版本(以我公司为例)。后续介绍以2.0
举例介绍。
版本 | 日期 | 笔记 |
---|---|---|
3.1.0 | 2021-02-15 | 发布 OpenAPI 规范 3.1.0 |
3.0.3 | 2020-02-20 | OpenAPI 规范 3.0.3 的补丁发布 |
3.0.2 | 2018-10-08 | OpenAPI 规范 3.0.2 的补丁发布 |
3.0.1 | 2017-12-06 | OpenAPI 规范 3.0.1 的补丁发布 |
3.0.0 | 2017-07-26 | 发布 OpenAPI 规范 3.0.0 |
2.0 | 2014-09-08 | Swagger 2.0 发布 |
1.2 | 2014-03-14 | 正式文件的初步发布 |
1.1 | 2012-08-22 | Swagger 1.1 的发布 |
1.0 | 2011-08-10 | Swagger 规范的第一个版本 |
规范(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的数据类型映射关系
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 字段 |
---|---|---|
functionName | operationId | logListUsingPOST |
path | basePath/path | /log/list |
method | paths—key-key | post |
接口名称注释 | summary | 日志列表 |
content-type | consumes | "application/json" |
parameters | parameters | 根据in字段判断body还是formdata, required是否必填, $refs关联具体类型,有什么字段 |
responses | responses | 一般只看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 自动生成接口请求代码