UNPKG

@lonu/stc

Version:

A tool for converting OpenApi/Swagger/Apifox into code.

373 lines (372 loc) 13.9 kB
import micromatch from "micromatch"; import Logs from "./console.js"; import { camelCase, getObjectKeyByValue, getRefType, hasKey, 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 getVirtualProperties = (defItem, defMapping, defs, defData) => { if (!defItem.type.includes("object")) { Logs.warn(getT("$t(def.parserTypeError)", { name: defMapping.name, type: defItem.type, })); return []; } const props = defItem.properties ?? {}; const mappings = defMapping.mappings ?? {}; const vProps = Object.keys(props).reduce((prev, current) => { const prop = props[current]; // 必填属性 const required = defItem.required?.includes(current) ?? false; // 属性枚举选项值 const enumOption = prop.enum || []; // 属性 ref let refName = getDefinitionNameMapping(prop.$ref ?? (typeof prop.additionalProperties === "object" && "$ref" in prop.additionalProperties ? prop.additionalProperties.$ref : "") ?? "") .name; if (prop.items) { refName = getDefinitionNameMapping(prop.items.$ref ?? "").name || (prop.items.type ?? ""); } // 属性类型。若存在枚举选项,则需要声明一个“定义名 + 属性名”的枚举类型 let type = enumOption.length ? camelCase(`${defMapping.name}_${current}`, true) : (getObjectKeyByValue(mappings, refName) || prop.type); // 如果 ref 的自定义类型为基础类型,且 type 为空 if (!type && !defs[refName].type.includes("object") && !defs[refName].enum?.length) { type = defs[refName].type; refName = ""; } const _defItem = { name: camelCase(current), type, description: prop.description ?? "", required, enumOption, ref: refName, format: prop.format ?? "", nullable: prop.nullable, }; // 处理当前属性的子属性 if (hasKey(prop, "properties")) { const _childDef = getDefinitionNameMapping(current, true); const _childProps = getVirtualProperties(prop, _childDef, defs, defData); // 将 type 中存在 object,替换为新名字 if (_defItem.type.includes("object") && _childProps.length) { const _objTypeName = defMapping.name + _childDef.name; if (Array.isArray(_defItem.type)) { const _objIndex = _defItem.type.indexOf("object"); _defItem.type.splice(_objIndex, 1, _objTypeName); } else { _defItem.type = _objTypeName; } defData.set(_objTypeName, _childProps); } } prev.push(_defItem); return prev; }, []); return vProps; }; /** * 生成定义对象 * @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); } 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 = (properties, requiredProps) => { const _properties = Object.keys(properties ?? {}) .reduce((prev, current) => { const _props = properties[current]; const _propItem = { name: current, type: _props?.type ?? "", typeX: _props.items?.type.toString(), required: requiredProps.includes(current) ?? false, title: _props?.title, description: _props?.description ?? "", // ref: getRefType( // _props?.$ref ?? _props?.items?.$ref ?? "", // ), }; // 处理 properties if (hasKey(_props, "properties")) { _propItem.properties = getProperties((_props.properties), (_props?.required ?? [])); } // 处理 items if (hasKey(_props, "items")) { // 检查 items 是否为 object if (hasKey(_props.items, "properties")) { _propItem.properties = getProperties((_props.items?.properties), (_props.items?.required ?? [])); } } prev.push(_propItem); return prev; }, []); return _properties; }; /** * 获取请求对象 * @param url - 接口地址 * @param method - 请求方式 * @param pathMethod - 请求对象 * @param tagIndex - 从 url 指定标签 * @returns */ const getPathVirtualProperty = (url, method, pathMethod, tagIndex) => { // 请求参数 path、query、body、formData、header const parameters = (pathMethod.parameters?.sort((_a, _b) => Number(_b.required) - Number(_a.required)) ?? []).reduce((prev, current) => { const _schema = current.schema; const item = { name: 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, }; prev[current.in].push(item); return prev; }, { path: [], query: [], body: [], formData: [], header: [] }); // v3 body 参数在 requestBody const _requestBody = pathMethod.requestBody; if (_requestBody) { Object.keys(_requestBody.content).forEach((_key) => { const _bodyContent = _requestBody.content[_key]; const _bodyContentSchema = _bodyContent?.schema; const _bodyContentRef = getRefType(_bodyContentSchema?.$ref ?? _bodyContentSchema?.items?.$ref ?? ""); // 处理 type 为 object 的情况,并且有 properties 属性 if (_bodyContentSchema?.type === "object" && !Object.keys(_bodyContentSchema?.properties ?? {}).length) return; const _name = (["application/octet-stream", "multipart/form-data"].includes(_key) ? "file" : lowerCase(_bodyContentRef)) || "body"; const _type = _name === "file" ? "FormData" : _bodyContentSchema?.type ?? ""; const _properties = getProperties(_bodyContentSchema?.properties ?? {}, _bodyContentSchema?.required ?? []); 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?.properties ?? _resSchema?.items?.properties ?? {}, _resSchema?.required ?? []); // 标签,用于文件名 let _tag = pathMethod.tags?.[0]; if (tagIndex !== undefined) { _tag = url.split("/")[tagIndex]; } 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 - 接口地址 * @returns */ export const getApiPath = (paths, options) => { 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) => { // url去除 `?` 之后的字符 if (url.includes("?")) url = url.slice(0, url.indexOf("?")); const currentMethod = methods[method]; // 方法名 let name = currentMethod.operationId ?? getMethodName(url, options.conjunction, options?.actionIndex); if (!name) { Logs.error(getT("$t(path.notName)", { url, method })); return; } // 添加请求方式标识,如 GET,POST 等,防止重名。设置了 operationId,以 operationId 为准 if (!currentMethod.operationId && !/^(get|post|put|delete|options|head|patch)/i.test(name.slice(0, method.length))) { name = `${method}${upperCase(name)}`; } // 接口对象 const value = getPathVirtualProperty(url, method, currentMethod, options?.tag); 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