generateApiFromOpenAPI.js 10.8 KB
/**
 * 从 OpenAPI 文档自动生成 API 接口文件
 *
 * 功能:
 * 1. 扫描 docs/openAPI 目录
 * 2. 解析每个 .md 文件中的 OpenAPI YAML 规范
 * 3. 提取 API 信息并生成对应的 JavaScript API 文件
 * 4. 保存到 src/api/ 目录
 *
 * 目录结构:
 * docs/openAPI/
 *  ├── module1/
 *  │   ├── api1.md
 *  │   └── api2.md
 *  └── module2/
 *      └── api3.md
 *
 * 生成到:
 * src/api/
 *  ├── module1.js
 *  └── module2.js
 */

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

/**
 * 提取 Markdown 文件中的 YAML 代码块
 * @param {string} content - Markdown 文件内容
 * @returns {string|null} - YAML 字符串或 null
 */
function extractYAMLFromMarkdown(content) {
  const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/;
  const match = content.match(yamlRegex);
  return match ? match[1] : null;
}

/**
 * 将字符串转换为驼峰命名
 * @param {string} str - 输入字符串
 * @returns {string} - 驼峰命名字符串
 */
function toCamelCase(str) {
  return str
    .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
    .replace(/^(.)/, (c) => c.toLowerCase());
}

/**
 * 将字符串转换为帕斯卡命名(首字母大写)
 * @param {string} str - 输入字符串
 * @returns {string} - 帕斯卡命名字符串
 */
function toPascalCase(str) {
  const camelCase = toCamelCase(str);
  return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
}

/**
 * 解析对象属性,生成字段描述
 * @param {object} properties - 属性对象
 * @param {number} indent - 缩进级别
 * @returns {string} - 字段描述字符串
 */
function parseProperties(properties, indent = 0) {
  if (!properties) return '';

  const lines = [];
  const prefix = '  '.repeat(indent);

  Object.entries(properties).forEach(([key, value]) => {
    const type = value.type || 'any';
    const desc = value.description || value.title || '';
    const required = value.required ? '' : ' (可选)';

    // 基本类型
    if (type !== 'object' && type !== 'array') {
      lines.push(`${prefix}${key}: ${type}${required} - ${desc}`);
    }
    // 对象类型
    else if (type === 'object' && value.properties) {
      lines.push(`${prefix}${key}: {`);
      lines.push(`${prefix}  // ${desc}`);
      lines.push(parseProperties(value.properties, indent + 2));
      lines.push(prefix + '}');
    }
    // 数组类型
    else if (type === 'array' && value.items) {
      const itemType = value.items.type || 'any';
      if (itemType === 'object' && value.items.properties) {
        lines.push(`${prefix}${key}: Array<{`);
        lines.push(`${prefix}  // ${desc}`);
        lines.push(parseProperties(value.items.properties, indent + 2));
        lines.push(prefix + '}>');
      } else {
        lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`);
      }
    }
  });

  return lines.join('\n');
}

/**
 * 生成 JSDoc 参数注释
 * @param {Array} parameters - 参数数组
 * @returns {string} - JSDoc 参数注释
 */
function generateParamJSDoc(parameters) {
  if (!parameters || parameters.length === 0) {
    return ' * @param {Object} params 请求参数';
  }

  const lines = [' * @param {Object} params 请求参数'];

  // 过滤掉 a 和 f 参数,因为它们已经在 URL 中了
  const filteredParams = parameters.filter(p => p.name !== 'a' && p.name !== 'f');

  if (filteredParams.length === 0) {
    return lines.join('\n');
  }

  filteredParams.forEach((param) => {
    const type = param.schema?.type || 'any';
    const desc = param.description || '';
    const required = param.required ? '' : ' (可选)';
    lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
  });

  return lines.join('\n');
}

/**
 * 生成 JSDoc 返回值注释
 * @param {object} responseSchema - 响应 schema
 * @returns {string} - JSDoc 返回值注释
 */
function generateReturnJSDoc(responseSchema) {
  if (!responseSchema || !responseSchema.properties) {
    return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回';
  }

  const { code, msg, data } = responseSchema.properties;

  let returnDesc = ' * @returns {Promise<{\n';
  returnDesc += ' *   code: number; // 状态码\n';
  returnDesc += ' *   msg: string; // 消息\n';

  if (data && data.properties) {
    returnDesc += ' *   data: {\n';

    Object.entries(data.properties).forEach(([key, value]) => {
      const type = value.type || 'any';
      const desc = value.description || value.title || '';

      if (type === 'object' && value.properties) {
        returnDesc += ` *     ${key}: {\n`;
        Object.entries(value.properties).forEach(([subKey, subValue]) => {
          const subType = subValue.type || 'any';
          const subDesc = subValue.description || subValue.title || '';
          returnDesc += ` *       ${subKey}: ${subType}; // ${subDesc}\n`;
        });
        returnDesc += ` *     };\n`;
      } else if (type === 'array' && value.items && value.items.properties) {
        returnDesc += ` *     ${key}: Array<{\n`;
        Object.entries(value.items.properties).forEach(([subKey, subValue]) => {
          const subType = subValue.type || 'any';
          const subDesc = subValue.description || subValue.title || '';
          returnDesc += ` *       ${subKey}: ${subType}; // ${subDesc}\n`;
        });
        returnDesc += ` *     }>;\n`;
      } else {
        returnDesc += ` *     ${key}: ${type}; // ${desc}\n`;
      }
    });

    returnDesc += ' *   };\n';
  } else {
    returnDesc += ' *   data: any;\n';
  }

  returnDesc += ' * }>}';

  return returnDesc;
}

/**
 * 解析 OpenAPI 文档并提取 API 信息
 * @param {object} openapiDoc - 解析后的 OpenAPI 对象
 * @param {string} fileName - 文件名(用作 API 名称)
 * @returns {object} - 提取的 API 信息
 */
function parseOpenAPIDocument(openapiDoc, fileName) {
  try {
    const path = Object.keys(openapiDoc.paths)[0];
    const method = Object.keys(openapiDoc.paths[path])[0];
    const apiInfo = openapiDoc.paths[path][method];

    // 提取 query 参数
    const parameters = apiInfo.parameters || [];
    const queryParams = {};
    let actionValue = '';

    parameters.forEach((param) => {
      if (param.in === 'query') {
        queryParams[param.name] = param.example || param.schema?.default || '';

        // 提取 action 参数(通常是 'a' 参数)
        if (param.name === 'a') {
          actionValue = param.example || '';
        }
      }
    });

    // 提取响应结构
    const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;

    return {
      summary: apiInfo.summary || fileName,
      description: apiInfo.description || '',
      method: method.toUpperCase(),
      action: actionValue,
      queryParams,
      parameters, // 保存完整的参数信息用于生成 JSDoc
      responseSchema, // 保存响应结构用于生成 JSDoc
      fileName,
    };
  } catch (error) {
    console.error(`解析 OpenAPI 文档失败: ${error.message}`);
    return null;
  }
}

/**
 * 生成 API 文件内容
 * @param {string} moduleName - 模块名称
 * @param {Array} apis - API 信息数组
 * @returns {string} - 生成的文件内容
 */
function generateApiFileContent(moduleName, apis) {
  const imports = `import { fn, fetch } from '@/api/fn';\n\n`;
  const apiConstants = [];
  const apiFunctions = [];

  apis.forEach((api) => {
    // 生成常量名(帕斯卡命名)
    const constantName = toPascalCase(api.fileName);
    // 生成函数名(驼峰命名 + API 后缀)
    const functionName = toCamelCase(api.fileName) + 'API';

    // 添加常量定义
    apiConstants.push(
      `  ${constantName}: '/srv/?a=${api.action}',`
    );

    // 生成详细的 JSDoc 注释
    const paramJSDoc = generateParamJSDoc(api.parameters);
    const returnJSDoc = generateReturnJSDoc(api.responseSchema);

    // 添加函数定义
    const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post';
    const comment = `/**
 * @description: ${api.summary}
${paramJSDoc}
${returnJSDoc}
 */`;

    apiFunctions.push(`${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));`);
  });

  return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n`;
}

/**
 * 扫描目录并处理所有 OpenAPI 文档
 * @param {string} openAPIDir - OpenAPI 文档目录
 * @param {string} outputDir - 输出目录
 */
function scanAndGenerate(openAPIDir, outputDir) {
  if (!fs.existsSync(openAPIDir)) {
    console.error(`OpenAPI 目录不存在: ${openAPIDir}`);
    return;
  }

  // 确保输出目录存在
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  // 扫描第一级目录(模块)
  const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
    .filter(dirent => dirent.isDirectory())
    .map(dirent => dirent.name);

  console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`);

  modules.forEach((moduleName) => {
    const moduleDir = path.join(openAPIDir, moduleName);
    const apiFiles = fs.readdirSync(moduleDir)
      .filter(file => file.endsWith('.md'));

    if (apiFiles.length === 0) {
      console.log(`模块 ${moduleName} 中没有找到 .md 文件`);
      return;
    }

    console.log(`\n处理模块: ${moduleName}`);
    console.log(`找到 ${apiFiles.length} 个 API 文档`);

    const apis = [];

    apiFiles.forEach((fileName) => {
      const filePath = path.join(moduleDir, fileName);
      const content = fs.readFileSync(filePath, 'utf8');
      const yamlContent = extractYAMLFromMarkdown(content);

      if (!yamlContent) {
        console.warn(`  ⚠️  ${fileName}: 未找到 YAML 代码块`);
        return;
      }

      try {
        const openapiDoc = yaml.load(yamlContent);
        const apiName = path.basename(fileName, '.md');
        const apiInfo = parseOpenAPIDocument(openapiDoc, apiName);

        if (apiInfo) {
          apis.push(apiInfo);
          console.log(`  ✓ ${apiName}: ${apiInfo.summary}`);
        }
      } catch (error) {
        console.error(`  ✗ ${fileName}: 解析失败 - ${error.message}`);
      }
    });

    // 生成并保存 API 文件
    if (apis.length > 0) {
      const fileContent = generateApiFileContent(moduleName, apis);
      const outputPath = path.join(outputDir, `${moduleName}.js`);
      fs.writeFileSync(outputPath, fileContent, 'utf8');
      console.log(`  📝 生成文件: ${outputPath}`);
    }
  });

  console.log('\n✅ API 文档生成完成!');
}

// 执行生成
const openAPIDir = path.resolve(__dirname, '../docs/openAPI');
const outputDir = path.resolve(__dirname, '../src/api');

console.log('=== OpenAPI 转 API 文档生成器 ===\n');
console.log(`输入目录: ${openAPIDir}`);
console.log(`输出目录: ${outputDir}\n`);

scanAndGenerate(openAPIDir, outputDir);