apifox-to-ts

A small tool that generates typescript declarations from apifox's interface documentation, and records the development process.

Repository code: github.com/JexLau/apif…

background

The project wants to access typescript development, but maintaining the interface typings is a tedious and repetitive process. After observing the interface documentation provided by the backend (based on the apifox platform), it is found that its data composition has certain rules, and the backend interface specification Good, thanks to the experience learned in the previous company, I decided to write a script tool to automatically generate interface typings for interface documents.

Of course, apifox also provides tools to generate typings, but I think there may be two flaws:

  1. For the processing of enumeration types, in actual development, some non-standard conversions of enumeration writing will report errors. Personally, I think it is better to treat it as a union type.
  2. The code generated by apifox, the schema and the interface layer are mixed together. When the project is large, it will be confusing. The corresponding schema and path generated by module will be clearer and easier to maintain.

result

To get a clearer sense of what this tool does, put the results first. The first file is the service.ts file, which is the interface file that can be directly introduced and used in the project. The second file is the schema file, which contains all the schemas of the project. The third file is the path file, which contains all the requests for the project. Copy these three files into the project and use them directly. Subsequent interfaces are updated, and execution scripts can also be generated with one click, eliminating the need to laboriously maintain interface typings.

The sharp-eyed friends of course found out that the current project has not generated corresponding files according to the modules. The reason is that during the development process, it was found that the module name is Chinese, and I do not want to generate modules according to Chinese, so I will not do it for the time being.

development process

Open an apifox project document, and by observing the request of the console, it is found that the data on the apifox web page mainly comes from these three interfaces:

  • /api/v1/shared-docs/${share_id}/data-schemas is used to request schemas (abstract collection)
  • /api/v1/shared-docs/${share_id}/http-api-tree is used to request all modules of the interface
  • /api/v1/shared-docs/ s h a r e i d / h t t p a p i s / {share_id}/http-apis/ {api_id} 用来请求某一接口的具体构成(api_id是通过上一个接口http-api-tree拿到的)

share_id 为项目分享生成的 id

在日常开发中,我们知道一个请求主要有这两部分:请求参数Request+请求返回值Response,请求Path。第一部分可以抽象地看成是Schema(抽象集合),第二部分就是Path,工具函数需要做的就是解析这两部分。

1. 解析schema

上一步我们知道 data-schemas 这个接口可以拿到项目中所有 schemas,我放个具体的截图感受一下。左边是整体的截图,右边是某一项的截图。

它是一个数组,每个数组项就是一个 schema,所谓的 schema 就是一个对象,解析 schema 就是解析这个对象里面的属性。解析属性,首先需要知道这个属性是什么类型,我们可以先把所有的 type 打印出来看一下。

// 抽离schemas
const schemasUrl = "https://www.apifox.cn/api/v1/shared-docs/xxxx/data-schemas"
axios.get(schemasUrl).then(res => {
  const schemasData = res.data.data;
  console.log(`**************成功请求 schemas 数据**************`);
  // 处理schema
  // 先观察一下schema有多少种type
  const types = []
  schemasData.forEach(item => {
    const properties = item.jsonSchema.properties;
    if (properties) {
      for (let p in properties) {
        const type = properties[p].type
        if(types.indexOf(type) === -1) {
          types.push(type)
        }
      }
    }
  })
  console.log(types.join(",")) // string,array,integer,,object,boolean
})
复制代码

打印出来的结果是 string,array,integer,,object,boolean。注意,里面有一个空值,待会我们可以打印一下空值的jsonSchema是什么情况。string, integer,boolean都是简单类型,可以直接转换为typescript里面对应的string, number,boolean类型,但对于object,array和空值,我们需要额外去处理它。那么我们先看一下这三种类型是什么情况:

这是它们的结构,可以看到这三者的结构是不一样的,可以根据这些数据大概抽象出它们的interface。而 { '$ref': '#/definitions/5227172' } 这种结构,意思是它的类型对应的是 id 为 5227172 的 schema。

interface SchemaArray {
  type: string, // 目前发现有 'array' | 'object' 这两个类型的值
  example?: string[],
  description?: string,
  items: { 
    type?: string, // 简单类型
    $ref?: string, // 链接另外一个scheme,和type互斥
  }
}

interface SchemaObject {
  type: string,
  example?: {},
  description?: string,
  additionalProperties: { 
    type?: string, // 'object'
  }
}

interface SchemaNull {
  $ref?: string, // 链接另外一个scheme
}
复制代码

理清了这些类型关系,我们可以对这个schema对象数组简单做一个解析,解析的结果就是每一个 schema 数据对应的 interface/type。

解析类型

经过上面的过程,我们知道有 string,array,integer,,object,boolean 这几种类型。通过观察数据,发现type为string其中还有一种情况,就是枚举enum。所以我们首先要先把schema-data遍历一次,生成所有的枚举类型(方便后面引用)。

for (let key in properties) {
  const property = properties[key]
  if (property.enum) {
    // schemaTitle充当一个前缀的作用,防止枚举重命名
    const enumName = schemaTitle + firstToLocaleUpperCase(key)
    const description = property.description || ""
    result += `
/** ${description} */
type ${enumName} = ${handleEnumType(property.enum)}`
  }
}
复制代码

处理完枚举类型,然后再遍历一次,根据对应的type生成typescript的代码。

schemasData.forEach(item => {
  const properties = item.jsonSchema.properties;
  const required = item.jsonSchema.required;
  const description = item.jsonSchema.description || "";
  const schemaTitle = formatSchemaName(item.jsonSchema.title);

  result += `
  /** ${description} */
  interface ${schemaTitle} {${handleAllType(properties, required, schemaTitle)}
  }`
})

/** 转换类型 */
const convertType = function (property, key, schemaTitle = "") {
  let type = "未知";
  switch (property.type) {
    case "string":
      if (property.enum) {
        const enumType = schemaTitle + firstToLocaleUpperCase(key)
        type = enumType
      } else {
        type = "string"
      };
      break;
    case "boolean":
      type = "boolean";
      break;
    case "integer":
      type = "number";
      break;
    case "number":
      type = "number";
      break;
    case "array":
      if (property.items.type) {
        let itemType = property.items.type;
        if (itemType === "integer") {
          type = `Array<number>`;
        } else {
          type = `Array<${itemType}>`;
        }
      } else if (property.items.$ref) {
        const refType = convertRefType(property.items.$ref);
        if (refType) {
          type = `Array<${refType}>`;
        }
      }
      break;
    case "object":
      if (property.additionalProperties && property.additionalProperties.type) {
        // 递归遍历
        type = convertType(property.additionalProperties);
      } else {
        // 任意object
        type = "{[key: string]: object}"
      }
      break;
    default:
      if (property.$ref) {
        const refType = convertRefType(property.$ref);
        if (refType) {
          type = refType;
        }
      }
  }
  // formatSchemaName 作用是对命名格式化,去除一些特殊符号
  return formatSchemaName(type);
}
复制代码

就可以生成所有的schema了。然后使用writeFileSync()写入到目标文件中。

2. 解析path

apifox数据结构是,先拿到api-tree,然后轮询id获取请求的request和response。所以第一步是拿到api-tree的数据,然后取出模块id轮询获取api接口的数据。

拿到数据之后,就是转换path文件。一个请求,最重要的就是请求参数和请求返回值。所以需要生成对应的Request和Response。正常情况下,传参有三种位置,path,query,body,path和query只能传递字符串,body一般是一个请求体(可以看作是一个schema),body的schema在前面生成的schema中可以找得到,所以直接引用就可以。(解析就是纯力气活,根据数据格式解析就完事了)

/** 转换Path */
const convertPaths = (item) => {
  let cacheApiName = [];
  const getApiName = createApiName(item.path, item.method);
  let pathsFileCotent = `
    /**
    ** 接口名称: ${item.name}
    ** 接口地址: ${item.path}
    ** 请求方式: ${item.method}
    ** 接口描述: ${item.description}
    */
    namespace ${getApiName(cacheApiName)} {
      /** 请求 */
      interface Request ${convertRequstBody(item.requestBody)}{
        ${convertParameters(item.parameters)}
      }

      /** 响应 */
      interface Response ${convertResponse(item.responses)} {
      }
    }
    `
  return pathsFileCotent;
}

/** 转换body参数 */
function convertRequstBody(requestBody) {
  if (!requestBody || requestBody.type === "none") {
    return "";
  }
  if (requestBody.type === "application/json") {
    const bodyRef = requestBody.jsonSchema.$ref;
    const bodySchemaName = convertRefType(bodyRef)
    if (bodySchemaName) {
      return `extends Api.Schema.${bodySchemaName}`;
    }
  }
  return ""
}

// convertParameters 略
复制代码

解析response更简单了,一般返回值都是一个schema,直接把这个schema与前面的schema对应起来即可

function convertResponse(responses) {
  const successRes = responses.find(item => item.name === "OK");
  const resRef = successRes.jsonSchema.$ref || "";
  const resSchemaName = convertRefType(resRef)
  if (resSchemaName) {
    return `extends Api.Schema.${resSchemaName} `;
  }
  return ""
}
复制代码

此时可以生成所有接口的paths文件了。然后使用writeFileSync()写入到目标文件中。

3. Generate service file

At this point, you already have the schema and paths files and can actually use them in the project. But through practice, it is found that the service file can also be generated by certain rules, so there is no need to write the interface code so much.

function convertServices(item) {
  let cacheApiName = [];
  const getApiName = createApiName(item.path, item.method);
  const apiName = getApiName(cacheApiName);
  const servicesFileCotent = `
/**
** 接口名称: ${item.name}
** 接口地址: ${item.path}
** 请求方式: ${item.method}
** 接口描述: ${item.description}
*/
export function ${apiName} (params: Api.Paths.${apiName}.Request) {
  return request<Api.Paths.${apiName}.Response>(
    `${item.path.replace(/[{]/g, "${params.")}`,
    {
      method: "${item.method.toUpperCase()}",
      ${["GET", "DELETE"].includes(item.method.toUpperCase()) ? "params," : "data: params,"}
    }
  );
}
        `;
  return servicesFileCotent;
}
复制代码

The generated code looks like the result graph shown earlier.

reward

A friendlier development experience (better fishing).

Guess you like

Origin juejin.im/post/7084828685596885029