UNPKG

@lonu/stc

Version:

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

303 lines (302 loc) 11 kB
import { camelCase, upperCase } from "../utils.js"; import { convertType, renderEtaString } from "./common.js"; import Logs from "../console.js"; import { getT } from "../i18n/index.js"; import { convertValue } from "../utils.js"; let pluginOptions; /** * 从给定的属性数组中获取属性,生成内部定义 * * @param {IDefinitionVirtualProperty[]} properties - 属性 * @param {string} name - 定义的名称 */ const getInternalDefinition = (properties, name) => { let _defHeader = "", _defFooter = ""; if (properties.length) { _defHeader = renderEtaString(pluginOptions.template.definitionHeader, { defName: name, }); _defFooter = renderEtaString(pluginOptions.template.definitionFooter, { defName: name, props: properties, }); } const _defs = properties.reduce((prev, current) => { let _type = convertType(current.type, current.typeX ?? current.ref, pluginOptions); if (current.properties?.length) { const _defName = `${name}${upperCase(current.name)}`; _type = convertType(current.type, _defName, pluginOptions); const _childDefinition = getInternalDefinition(current.properties, _defName); prev.childDefinitions.push(..._childDefinition.definitions, ..._childDefinition.childDefinitions); } const _defBody = renderEtaString(pluginOptions.template.definitionBody, { propCommit: current.title || current.description, propType: _type, prop: current, }); prev.definitions.splice(prev.definitions.length - 1, 0, _defBody); return prev; }, { definitions: properties.length ? [_defHeader, _defFooter] : [], childDefinitions: [], }); return _defs; }; /** * 从给定的属性数组中获取属性,生成内部定义 * * @param {IDefinitionVirtualProperty[]} properties - 属性 * @param {string} name - 定义的名称 */ const getDefinition = (properties, name) => { const _def = getInternalDefinition(properties, name); const _defs = [..._def.definitions, ..._def.childDefinitions]; return _defs; }; /** * 解析参数 * @param parameters - 参数 * @param action - 方法名称 */ const parseParams = (parameters, action) => Object.keys(parameters).reduce((prev, current) => { const _category = current; const _params = parameters[_category]; const _multiParam = _params.length > 1; const _defName = camelCase(`${action}_${current}_params`, true); // 形参 let _formalParam = { name: current, category: current, type: _defName, description: "", required: true, }; _params.forEach((item, index) => { const _type = item.enumOption?.length ? camelCase(`${_defName}_${item.name}`, true) : `${convertType(item.type, item.typeX ?? item.ref, pluginOptions)}`; // 外部引用 if (item.ref && !prev.imports?.includes(item.ref)) { prev.imports?.push(item.ref); } /* #region 内部定义 */ // 定义参数枚举 if (item.enumOption?.length) { const _enumData = renderEtaString(pluginOptions.template.enum, { name: _type, data: item.enumOption, convertValue, isEnum: true }); prev.definitions?.push(_enumData); } // properties 存在时直接定义 if (item.properties?.length) { const _defs = getDefinition(item.properties, _defName); prev.definitions?.push(_defs.join("\n")); } // 同类型的参数进行合并成新对象 if (_multiParam) { if (index === 0) { prev.definitions?.push(renderEtaString(pluginOptions.template.definitionHeader, { defName: _defName, })); } prev.definitions?.push(renderEtaString(pluginOptions.template.definitionBody, { propCommit: item.title || item.description, prop: item, propType: _type, })); if (index === _params.length - 1) { prev.definitions?.push(renderEtaString(pluginOptions.template.definitionFooter, { defName: _defName, props: _params, })); } } /* #endregion */ if (!_multiParam) { _formalParam = { name: item.name, category: current, type: _type, description: (item.title || item.description) ?? "", required: item.required ?? false, }; if (item.required) { // 必填参数 prev.requiredParams?.push(_formalParam); } else { // 可选参数 prev.optionalParams?.push(_formalParam); } } }); if (_multiParam) { prev.requiredParams?.push(_formalParam); } return prev; }, { imports: [], requiredParams: [], optionalParams: [], definitions: [], }); /** * 解析响应对象 * @param ref - 自定义类型 * @returns */ const parseResponseRef = (ref) => { const _sliceIndex = ref.indexOf("«"); const _imports = []; const _import = ref.slice(0, _sliceIndex > -1 ? _sliceIndex : undefined); if (_import && _import !== "Array") { _imports.push(_import); } const name = ref.replace(/«(.*)?»/g, (_key, _value) => { _value = _value.replace(/^List/, "Array"); const res = parseResponseRef(_value); _imports.push(...res.import); const arr = res.name.split(/,\s*/g).map((_ref) => { return _ref; }); return `<${arr.join(", ")}>`; }); return { name, import: _imports }; }; const parseResponse = (response, action) => { let _response; if (response.properties?.length) { const _defName = `${upperCase(action)}Response`; const _definitions = getDefinition(response.properties, _defName); _response = { name: _defName, type: _defName, definitions: _definitions, }; } else { const _defName = parseResponseRef(response.ref ?? ""); const _defNameType = convertType(response.type ?? "", _defName.name, pluginOptions); _response = { name: _defName.name, type: _defNameType, imports: _defName.import, }; } return _response; }; /** * 生成 Api * @param data - 接口数据 * @param action - 接口名称 * @returns */ const generateApi = (data, action) => { const methodName = data.method.toUpperCase(); Logs.info(`【${methodName}】${data.url}`); const _params = parseParams(data.parameters ?? {}, action); const _response = parseResponse(data.response, action); if (!_response.name) { Logs.warn(getT("$t(plugin.no_200_response)")); } const _apiMethod = renderEtaString(pluginOptions.template.actionMethod, { summary: data.summary, description: data.description, methodName: action, params: [ ..._params.requiredParams ?? [], ..._params.optionalParams ?? [], ], responseName: _response.name, responseType: _response.type, action: action, url: data.url, method: data.method, deprecated: data.deprecated, }); return { imports: [..._params.imports ?? [], ..._response.imports ?? []], definition: [ ...(_params.definitions ?? []), ...(_response.definitions ?? []), ] ?.join("\n"), method: _apiMethod, }; }; /** * Generates a map of action files based on the provided data. * * @param {Map<string, IPathVirtualProperty>} data - The data used to generate the action files. * @return {Map<string, IApiFile>} - A map of action files, where the key is the tag and the value is the corresponding IApiFile object. */ const getActionFiles = (data) => { const _actionFileMap = new Map(); Logs.info(`${getT("$t(plugin.parserAction)")}...`); data.forEach((item, key) => { const _tag = item.tag; if (!_tag) { Logs.error(getT("$t(plugin.no_tag)", { url: item.url })); return; } const actionName = key.slice(key.indexOf("@") + 1); const _apiData = generateApi(item, actionName); const _actionFile = _actionFileMap.get(_tag); if (_actionFile) { if (_apiData.imports) { // 导入内容去重 const _imports = Array.from(new Set([..._actionFile.imports, ..._apiData.imports])); _actionFile.imports = _imports; } _apiData.definition && _actionFile?.definitions?.push(_apiData.definition); _actionFile?.methods?.push(_apiData.method); } else { _actionFileMap.set(_tag, { imports: _apiData.imports, definitions: _apiData.definition ? [_apiData.definition] : [], methods: [_apiData.method], }); } }); Logs.info(getT("$t(plugin.parserActionDone)")); return _actionFileMap; }; /** * Parses actions from the given data and generates a map of action content. * * @param {Map<string, IPathVirtualProperty>} data - The data to parse actions from. * @param {string} defFileName - The name of the definition file. * @return {Map<string, string>} A map of action content where the key is the action file name and the value is the action content. */ export const parserActions = (data, defFileName, options) => { pluginOptions = options; const _actionContentMap = new Map(); const _actionFileMap = getActionFiles(data); _actionFileMap.forEach((action, key) => { // 处理导入文件相对路径,根据 key 中是否存在 `/` const _keyPath = key.split("/"); // 移除一项 _keyPath.pop(); const _importPath = _keyPath.reduce((prev, _) => { if (prev === "./") { prev = ""; } prev += "../"; return prev; }, "./"); const _apiImport = [ renderEtaString(pluginOptions.template.actionImport, { importPath: _importPath, imports: action.imports, typeFileName: defFileName, }), ]; const _apiContent = []; _apiContent.push(_apiImport.join("\n")); action.definitions?.length && _apiContent.push(action.definitions?.join("\n")); action.methods?.length && _apiContent.push(action.methods.join("\n")); _actionContentMap.set(`${key}.${options.lang}`, _apiContent.join("\n")); }); return _actionContentMap; };