""" Controller 端点解析模块。 调用 Java AST 解析器 JAR,将 Java 源码转为结构化的 API 端点列表。 """ import json import subprocess from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional @dataclass class ApiParameter: """单个接口参数。""" name: str type: str required: bool = True source: str = "query" description: Optional[str] = None @dataclass class ApiEndpoint: """单个 Controller 接口端点。""" http_method: str uri: str controller_class: str method_name: str source_file: str parameters: List[ApiParameter] = field(default_factory=list) @property def endpoint_key(self) -> str: """唯一标识:HTTP 方法 + URI,用于跨版本匹配。""" return f"{self.http_method} {self.uri}" def run_java_parser(source_dir: Path, jar_path: Path, output_json: Path) -> List[ApiEndpoint]: """ 调用 JavaParser JAR 扫描源码目录,返回解析结果。 :param source_dir: Java 源码根目录 :param jar_path: controller-parser JAR 路径 :param output_json: 临时 JSON 输出路径 :return: ApiEndpoint 列表 :raises RuntimeError: Java 进程失败或 JAR 不存在 """ if not jar_path.exists(): raise RuntimeError( f"Java 解析器 JAR 不存在: {jar_path}\n" "请先在 .gitea/java-parser 目录执行: mvn -q package" ) cmd = ["java", "-jar", str(jar_path), str(source_dir), str(output_json)] result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8") if result.returncode != 0: raise RuntimeError(f"Java 解析器执行失败:\n{result.stderr}") with open(output_json, "r", encoding="utf-8") as f: raw = json.load(f) return [_dict_to_endpoint(item) for item in raw] def _dict_to_endpoint(data: dict) -> ApiEndpoint: """将 JSON 字典转换为 ApiEndpoint 对象。""" params = [ ApiParameter( name=p.get("name", ""), type=p.get("type", ""), required=p.get("required", True), source=p.get("source", "query"), description=p.get("description"), ) for p in data.get("parameters", []) ] return ApiEndpoint( http_method=data.get("httpMethod", "GET"), uri=data.get("uri", "/"), controller_class=data.get("controllerClass", ""), method_name=data.get("methodName", ""), source_file=data.get("sourceFile", ""), parameters=params, ) def endpoints_to_map(endpoints: List[ApiEndpoint]) -> Dict[str, ApiEndpoint]: """ 将端点列表转为字典,key 为 endpoint_key。 :param endpoints: 端点列表 :return: { "GET /api/users/{id}": ApiEndpoint, ... } """ return {ep.endpoint_key: ep for ep in endpoints} def filter_endpoints_by_files( endpoints: List[ApiEndpoint], changed_files: List[str] ) -> List[ApiEndpoint]: """ 仅保留源文件在变更列表中的端点(缩小对比范围)。 :param endpoints: 全部端点 :param changed_files: 变更文件相对路径列表 :return: 过滤后的端点 """ if not changed_files: return endpoints changed_set = set(changed_files) return [ep for ep in endpoints if ep.source_file in changed_set]