@lonu/stc
Version:
A tool for converting OpenApi/Swagger/Apifox into code.
428 lines (427 loc) • 17.3 kB
JavaScript
import micromatch from "micromatch";
import Logs from "./console.js";
import { camelCase, getObjectKeyByValue, getRefType, lowerCase, upperCase, } from "./utils.js";
import { getT } from "./i18n/index.js";
// #region 处理定义数据
/**
* 获取定义
* @param key - 定义的名称
* @param isDefinition - 是否为定义
* @returns
*/
const getDefinitionNameMapping = (key, isDefinition) => {
const genericKey = ["T", "K", "U"];
const keyLength = genericKey.length;
const name = getRefType(key);
let mappings = {};
// 处理泛型
const newName = name.replace(/«(.*)?»/g, (_key, _value) => {
const def = getDefinitionNameMapping(_value, isDefinition);
// 定义的情况下,需要将具体名称换成 T、K、U...
if (isDefinition) {
mappings = def.mappings ?? {};
const arr = def.name.split(/,\s*/g).map((_n, index) => {
let newKey = genericKey[index % keyLength];
// 当超过预设泛型 key 长度,自动加数字
if (index >= keyLength) {
newKey = newKey + Math.ceil((index - keyLength) / keyLength);
}
if (!mappings[newKey]) {
mappings[newKey] = _n;
}
return newKey;
});
return `<${arr.join(", ")}>`;
}
return `<${def.name}>`;
});
return {
name: newName,
mappings,
};
};
/**
* 原始定义对象转换为虚拟定义对象
*
* @param defItem - 定义名的属性
* @param defMapping - 定义
* @returns
*/
const getVirtualPropertiesFromSchema = (schema, defs, defMapping, defData, requiredProps = [], visitedRefs = new Set()) => {
if (!schema)
return [];
const hasObjectShape = !!schema.properties && Object.keys(schema.properties).length > 0 ||
!!schema.allOf?.length || schema.type?.includes("object") ||
schema.type === "array";
if (!hasObjectShape && !schema.$ref) {
return [];
}
const mappings = defMapping.mappings ?? {};
const mergedProps = new Map();
const required = new Set(requiredProps);
const collectProps = (currentSchema) => {
if (!currentSchema)
return;
if (currentSchema.$ref) {
const refName = getRefType(currentSchema.$ref);
if (visitedRefs.has(refName))
return;
visitedRefs.add(refName);
const refSchema = defs[refName];
if (refSchema) {
collectProps(refSchema);
}
visitedRefs.delete(refName);
return;
}
(currentSchema.required ?? []).forEach((item) => required.add(item));
Object.keys(currentSchema.properties ?? {}).forEach((current) => {
const prop = currentSchema.properties?.[current];
const propName = current;
const propRequired = required.has(propName);
const enumOption = prop.enum || [];
let refName = getDefinitionNameMapping(prop.$ref ?? "").name;
if (prop.items) {
refName = getDefinitionNameMapping(prop.items.$ref ?? "").name ||
(prop.items.format === "binary" ? "file" : (prop.items.type ?? ""));
}
let type = enumOption.length
? camelCase(`${defMapping.name}_${propName}`, true)
: (getObjectKeyByValue(mappings, refName) || prop.type);
// OpenAPI v3 文件上传:type: string + format: binary 归一为 file 类型
if (prop.format === "binary") {
type = "file";
}
if (!type && defs[refName] && !defs[refName].type.includes("object") &&
!defs[refName].enum?.length) {
type = defs[refName].type;
refName = "";
}
const additionalRef = typeof prop.additionalProperties === "object" &&
"$ref" in prop.additionalProperties
? getDefinitionNameMapping(prop.additionalProperties.$ref ?? "").name
: undefined;
const childDef = getDefinitionNameMapping(propName, true);
const childSchema = prop;
const childProps = getVirtualPropertiesFromSchema(childSchema, defs, childDef, defData, [], visitedRefs);
const _defItem = {
name: camelCase(propName),
originalName: propName,
type: type || "",
description: prop.description ?? "",
required: propRequired,
enumOption,
ref: refName,
format: prop.format ?? "",
nullable: prop.nullable,
additionalRef,
};
if (childProps.length) {
const _objTypeName = defMapping.name + childDef.name;
if (childSchema.type?.includes("object") || childSchema.properties ||
childSchema.allOf?.length || childSchema.$ref) {
if (Array.isArray(_defItem.type)) {
const _objIndex = _defItem.type.indexOf("object");
_defItem.type.splice(_objIndex, 1, _objTypeName);
}
else {
_defItem.type = _objTypeName;
}
_defItem.properties = childProps;
if (defData) {
defData.set(_objTypeName, childProps);
}
}
}
mergedProps.set(propName, _defItem);
});
currentSchema.allOf?.forEach((item) => collectProps(item));
};
collectProps(schema);
return Array.from(mergedProps.values()).map((item) => ({
...item,
required: required.has(item.originalName ?? item.name),
}));
};
const getVirtualProperties = (defItem, defMapping, defs, defData) => {
const hasObjectShape = !!defItem.properties && Object.keys(defItem.properties).length > 0 ||
!!defItem.allOf?.length ||
defItem.type?.includes("object");
if (!hasObjectShape) {
Logs.warn(getT("$t(def.parserTypeError)", {
name: defMapping.name,
type: defItem.type,
}));
return [];
}
return getVirtualPropertiesFromSchema(defItem, defs, defMapping, defData, defItem.required ?? []);
};
/**
* 生成定义对象
* @param definitions - 定义对象
* @returns
*/
export const getDefinition = (definitions) => {
const defMap = new Map();
Object.keys(definitions).forEach((key) => {
const def = getDefinitionNameMapping(key, true);
const name = def.name;
// 存在相同定义时,直接跳过
const defKeys = [];
defMap.forEach((_, key) => {
defKeys.push(key.replace(/<.*>$/, ""));
});
if (defKeys.includes(name))
return;
const defItem = definitions[key];
let props = [];
if (defItem.enum?.length) {
props = {
type: defItem.type,
enumOption: defItem.enum,
};
}
else {
props = getVirtualProperties(defItem, def, definitions, defMap);
}
if (def.name === "MetaVariableDataTypeMapResponse") {
console.log(props);
}
defMap.set(name, props);
});
return defMap;
};
// #endregion
// #region 处理所有的 url 数据
/**
* 从 URL 获取方法名称
* @param url - 接口地址
* @param conjunction - 连接字符
* @param index - 下标, 默认 -1
* @returns
*/
const getMethodName = (url, conjunction, index = -1) => {
let _url = url;
if (url.indexOf("?") > -1) {
_url = url.substring(0, url.indexOf("?"));
}
const _urls = _url.split("/");
let _name = _urls.slice(index).join("_");
if (!_name)
return _name;
const regExp = /[\\{|:](\w+)[\\}]/g;
const regReplace = /[\\{|:\\}]/g;
if (regExp.test(_url)) {
// 取最后一个动态路径
const _lastName = _url.match(regExp)?.pop()?.replace(regReplace, "");
// 若 _name 中存在动态路径,判断是否与 _lastName 重复
if (regExp.test(_name)) {
const _namePath = _name.match(regExp)?.reduce((prev, current) => {
const _n = current.replace(regReplace, "");
// 移除与 _lastName 重复的
if (_n !== _lastName) {
prev.push(_n);
}
return prev;
}, []).join("_") ?? "";
if (_namePath) {
_name = `${conjunction}_${_namePath}`;
}
else {
// 移除动态路径名
_name = _name.replace(regExp, "");
}
}
// 动态路径添加连接字符
_name = `${_name}_${conjunction}_${_lastName}`;
}
// 方法名小驼峰
return camelCase(_name);
};
/**
* Apifox 属性(type 为 object 时,处理存在的属性定义)
* @param properties - 属性
* @param requiredProps - 必填属性
* @returns
*/
const getProperties = (schema, requiredProps = [], definitions) => {
const defMapping = getDefinitionNameMapping("Body", true);
return getVirtualPropertiesFromSchema(schema, definitions ?? {}, defMapping, undefined, requiredProps);
};
/**
* 获取请求对象
* @param url - 接口地址
* @param method - 请求方式
* @param pathMethod - 请求对象
* @param options - 配置项
* @returns
*/
const getPathVirtualProperty = (url, method, pathMethod, options, definitions) => {
// 请求参数 path、query、body、formData、header
const parameters = (pathMethod.parameters?.sort((_a, _b) => Number(_b.required) - Number(_a.required)) ?? []).reduce((prev, current) => {
if ((current.in === "header" &&
!options?.globalHeader?.includes(current.name.toLowerCase())) ||
current.in !== "header") {
const _schema = current.schema;
const item = {
name: camelCase(current.name),
originalName: current.name,
type: current.type ?? _schema?.type ?? "",
required: current.required,
description: current.description,
format: current.format ?? _schema?.format,
ref: getRefType(_schema?.$ref ?? _schema?.items?.$ref ?? ""),
typeX: current?.items?.type ?? _schema?.items?.type,
default: _schema?.default,
enumOption: _schema?.enum,
};
const _schemaProperties = getProperties(_schema, _schema?.required ?? [], definitions);
if (_schemaProperties.length) {
item.properties = _schemaProperties;
}
prev[current.in].push(item);
}
return prev;
}, { path: [], query: [], body: [], formData: [], header: [] });
// v3 body 参数在 requestBody
const _requestBody = pathMethod.requestBody;
if (_requestBody) {
const _formContentTypes = [
"multipart/form-data",
"application/x-www-form-urlencoded",
];
// requestBody 下各内容类型是同一请求体的备选序列化方式:
// 已存在 application/json 等 body 内容类型时,表单类型不再展开为 formData,
// 避免同一接口同时生成 body 与 formData 两个参数
const _hasBodyContent = Object.keys(_requestBody.content).some((_key) => !_formContentTypes.includes(_key));
Object.keys(_requestBody.content).forEach((_key) => {
const _bodyContent = _requestBody.content[_key];
const _bodyContentSchema = _bodyContent?.schema;
const _bodyContentRef = getRefType(_bodyContentSchema?.$ref ?? _bodyContentSchema?.items?.$ref ?? "");
// 表单内容类型:schema 属性展开为 formData 参数,与 v2 的 in: formData 一致
if (_formContentTypes.includes(_key)) {
// 存在备选 body 内容类型时,不重复生成 formData 参数
if (_hasBodyContent)
return;
let _formDataProps = getProperties(_bodyContentSchema, _bodyContentSchema?.required ?? [], definitions);
// schema 无 properties 时(如直接声明 binary),退化为单个 file 参数
if (!_formDataProps.length) {
_formDataProps = [{
name: "file",
originalName: "file",
type: "file",
required: _requestBody.required ?? true,
description: _requestBody.description ?? "",
}];
}
_formDataProps.forEach((_prop) => {
// formData 存在相同 name 时,无需重复添加
if (!parameters.formData.some((item) => item.name === _prop.name)) {
parameters.formData.push(_prop);
}
});
return;
}
// 处理 type 为 object 的情况,并且有 properties 属性
if (_bodyContentSchema?.type === "object" &&
!Object.keys(_bodyContentSchema?.properties ?? {}).length)
return;
const _name = (_key === "application/octet-stream"
? "file"
: lowerCase(_bodyContentRef)) || "body";
const _type = _name === "file" ? "file" : _bodyContentSchema?.type ?? "";
const _properties = getProperties(_bodyContentSchema, _bodyContentSchema?.required ?? [], definitions);
const _body = {
name: _name,
type: _type,
required: _requestBody.required ?? true,
description: _requestBody.description,
ref: _bodyContentRef,
properties: _properties,
};
// body 存在相同 name 时,无需重复添加
if (!parameters.body.some((item) => item.name === _name)) {
parameters.body.push(_body);
}
});
}
// 响应
const _resSchema = pathMethod.responses[200]?.schema ??
pathMethod.responses[200]?.content?.["application/json"]?.schema ??
pathMethod.responses[200]?.content?.["text/plain"]?.schema;
const _properties = getProperties(_resSchema, _resSchema?.required ?? [], definitions);
// 标签,用于文件名
let _tag = pathMethod.tags?.[0];
if (options?.tag) {
_tag = url.split("/")[options?.tag];
}
const value = {
url,
method,
parameters,
requestHeaders: pathMethod.consumes,
responseHeaders: pathMethod.produces,
response: {
ref: getRefType(_resSchema?.$ref ?? _resSchema?.items?.$ref ?? ""),
type: _resSchema?.type,
properties: _properties,
},
summary: pathMethod.summary,
description: pathMethod.description,
tag: _tag,
deprecated: pathMethod.deprecated ?? false,
};
return value;
};
/**
* 获取接口地址对象
* @param paths - 接口地址
* @param options - 配置项
* @returns
*/
export const getApiPath = (paths, options, definitions) => {
const pathMap = new Map();
Object.keys(paths).forEach((url) => {
// 过滤接口,符合过滤条件的接口会被生成
if (options?.filter?.length && !micromatch.all(url, options.filter, {
bash: true,
}))
return;
// 请求方式
const methods = paths[url];
Object.keys(methods).forEach((method) => {
if (options?.noDeprecated && methods[method].deprecated)
return;
// url去除 `?` 之后的字符
if (url.includes("?"))
url = url.slice(0, url.indexOf("?"));
const currentAction = methods[method];
// 方法名
let name = currentAction.operationId ??
getMethodName(url, options.conjunction, options?.actionIndex);
if (!name) {
Logs.error(getT("$t(path.notName)", { url, method }));
return;
}
// 添加请求方式标识,如 GET,POST 等,防止重名。设置了 operationId,以 operationId 为准
if (!currentAction.operationId) {
name = name.replace(/^(get|post|put|delete|options|head|patch)/i, "");
name = `${method}${upperCase(name)}`;
}
// 接口对象
const value = getPathVirtualProperty(url, method, currentAction, options, definitions);
name = `${value.tag}@${name}`;
if (pathMap.has(name)) {
Logs.error(getT("$t(path.duplicate)", {
url,
method,
name: name.slice(name.indexOf("@") + 1),
}));
return;
}
pathMap.set(name, value);
});
});
return pathMap;
};
// #endregion