apifox-to-ts tool

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

Warehouse code: https://github.com/JexLau/apifox-to-ts

background

The project wants to access typescript development, but maintaining the typings of the interface is a cumbersome and repetitive process. After observing the interface documents provided by the backend (based on the apifox platform), it is found that its data composition has certain rules, and the interface specification of the backend 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 for generating typings, but I think there may be two flaws:

  1. For the processing of enumeration types, in actual development, some enumerations will report errors if the conversion is not standardized. Personally, I think it is better to treat it as a joint type.
  2. The code generated by apifox, the schema and the interface layer are mixed together. When the project is huge, it will be confusing. Generating the corresponding schema and path 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 imported 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 will be updated, and execution scripts can also be generated with one click, eliminating the need to laboriously maintain interface typings.

Of course, the sharp-eyed friends have discovered that the current project has not generated corresponding files based on the module. The reason is that during the development process, the module name is found to be in Chinese. I am unwilling to generate modules based on Chinese, so I will not do it for the time being.

development process

Open an apifox project document, and by observing the console request, 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/${share_id}/http-apis/${api_id} is used to request the specific composition of an interface (api_id is obtained through the previous interface http-api-tree)

share_id is the id generated for project sharing

In daily development, we know that a request mainly has two parts: request parameter Request + request return value Response, request Path. The first part can be regarded as Schema (abstract collection) abstractly, and the second part is Path. What the utility function needs to do is to parse these two parts.

1. Parsing the schema

In the previous step, we know that the data-schemas interface can get all the schemas in the project. Let me put a specific screenshot to feel it. On the left is a screenshot of the whole, and on the right is a screenshot of a certain item.

It is an array, and each array item is a schema. The so-called schema is an object, and parsing the schema is parsing the attributes in the object. To parse an attribute, you first need to know what type the attribute is. We can first print out all the types to have a look.

// 抽离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
})

The printed results are string, array, integer,, object, boolean. Note that there is a null value in it. We can print the jsonSchema of the null value later. string, integer, and boolean are simple types that can be directly converted to the corresponding string, number, and boolean types in typescript, but for object, array, and null values, we need to deal with them additionally. So let's take a look at what the three types are:

This is their structure. It can be seen that the structures of the three are different, and their interfaces can be roughly abstracted based on these data. The { '$ref': '#/definitions/5227172' } structure means that its type corresponds to the schema whose id is 5227172.

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
}

After sorting out these types of relationships, we can simply analyze the array of schema objects, and the result of the analysis is the interface/type corresponding to each schema data.

parsing type

After the above process, we know that there are string, array, integer,, object, boolean these types. By observing the data, it is found that there is another case where the type is string, which is to enumerate enum. So we first traverse the schema-data once to generate all enumerated types (for later reference).

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)}`
  }
}

After processing the enumeration type, it traverses it again, and generates typescript code according to the corresponding type.

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);
}

All schemas can be generated. Then use writeFileSync() to write to the target file.

2. Parse the path

The apifox data structure is to get the api-tree first, and then poll the id to get the request and response of the request. So the first step is to get the data of the api-tree, and then take out the module id and poll to get the data of the api interface.

After getting the data, it is to convert the path file. A request, the most important thing is the request parameters and request return value. So you need to generate the corresponding Request and Response. Under normal circumstances, there are three positions for passing parameters, path, query, body, path and query can only pass strings, body is generally a request body (can be regarded as a schema), and the schema of the body can be used in the previously generated schema You can find it, so you can just quote it directly. (Analysis is pure labor, and it is done according to the data format analysis)

/** 转换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 略

It is easier to parse the response. Generally, the return value is a schema, and you can directly match this schema with the previous 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 ""
}

At this point, the paths files of all interfaces can be generated. Then use writeFileSync() to write to the target file.

3. Generate service file

At this point, you already have the schema and paths files, which can be actually used in the project. But after practice, it is found that service files can also be generated according to certain rules, so there is no need to bother to write interface codes.

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

The development experience is more friendly (better fishing).

Guess you like

Origin blog.csdn.net/jexxx/article/details/128680808