AIDeveloper-PC/ai_test_generator/core/parser.py

395 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
接口描述解析器
══════════════════════════════════════════════════════════════
支持格式(当前版):
顶层字段:
- project : 项目名称,用于命名输出目录
- description : 项目描述
- units : 接口/函数描述列表
接口公共字段:
- name : 接口/函数名称
- requirement_id : 需求编号
- description : 接口描述
- type : "function" | "http"
- url : function → Python 源文件路径,如 "create_user.py"
http → base URL"http://127.0.0.1/api"
- parameters : 统一参数字段HTTP 与 function 两种协议共用)
⚠️ HTTP 接口不再使用 url_parameters / request_parameters
- return : 返回值描述(含 on_success / on_failure
HTTP 专用字段:
- method : "get" | "post" | "put" | "delete"
- 完整请求 URL = url.rstrip("/") + "/" + name.lstrip("/")
Function 专用:
- url 为 .py 文件路径,转换为 Python 模块路径供 import 使用
- 函数名即 name 字段
"""
from __future__ import annotations
import json
import re
from typing import Any
from dataclasses import dataclass, field
from config import config
# ══════════════════════════════════════════════════════════════
# 基础数据结构
# ══════════════════════════════════════════════════════════════
@dataclass
class ParameterInfo:
name: str
type: str # 支持复合类型 "string|integer"
description: str
required: bool = True
default: Any = None
inout: str = "in" # "in" | "out" | "inout"
@dataclass
class ReturnCase:
"""on_success 或 on_failure 的返回描述"""
value: Any = None
description: str = ""
@property
def is_exception(self) -> bool:
"""判断是否为抛出异常的场景"""
return (
isinstance(self.value, str)
and "exception" in self.value.lower()
)
@property
def value_fields(self) -> dict[str, Any]:
"""若 value 是 dict返回其字段否则返回空 dict"""
return self.value if isinstance(self.value, dict) else {}
@property
def value_summary(self) -> str:
"""返回值的简短文字描述,用于 LLM 提示词"""
if isinstance(self.value, dict):
return json.dumps(self.value, ensure_ascii=False)
if isinstance(self.value, bool):
return str(self.value).lower()
return str(self.value) if self.value is not None else ""
@dataclass
class ReturnInfo:
type: str = ""
description: str = ""
on_success: ReturnCase = field(default_factory=ReturnCase)
on_failure: ReturnCase = field(default_factory=ReturnCase)
@property
def all_fields(self) -> set[str]:
"""汇总 on_success + on_failure 中出现的所有字段名"""
fields: set[str] = set()
for case in (self.on_success, self.on_failure):
fields.update(case.value_fields.keys())
return fields
# ══════════════════════════════════════════════════════════════
# 接口描述
# ══════════════════════════════════════════════════════════════
@dataclass
class InterfaceInfo:
name: str
description: str
protocol: str # "http" | "function"
url: str = "" # 原始 url 字段
requirement_id: str = ""
# HTTP 专用
method: str = "get"
# 统一参数列表HTTP 与 function 两种协议共用,均来自 "parameters" 字段)
parameters: list[ParameterInfo] = field(default_factory=list)
# 返回值描述
return_info: ReturnInfo = field(default_factory=ReturnInfo)
# ── 便捷属性:参数分组 ────────────────────────────────────
@property
def in_parameters(self) -> list[ParameterInfo]:
"""inout = "in""inout" 的参数(作为输入)"""
return [p for p in self.parameters if p.inout in ("in", "inout")]
@property
def out_parameters(self) -> list[ParameterInfo]:
"""inout = "out""inout" 的参数(作为输出)"""
return [p for p in self.parameters if p.inout in ("out", "inout")]
# ── 便捷属性HTTP ────────────────────────────────────────
@property
def http_full_url(self) -> str:
"""
HTTP 协议的完整请求 URL
url (base) + name (path)
url="http://127.0.0.1/api", name="/delete_user"
"http://127.0.0.1/api/delete_user"
"""
if self.protocol != "http":
return ""
base = self.url.rstrip("/")
path = self.name.lstrip("/")
return f"{base}/{path}" if base else f"/{path}"
# ── 便捷属性Function ────────────────────────────────────
@property
def module_path(self) -> str:
"""
将 url.py 文件路径)转换为 Python 模块导入路径。
规则:
1. 去除 .py 后缀
2. 去除前导 ./ 或 /
3. 将路径分隔符替换为 .
示例:
"create_user.py""create_user"
"myapp/services/user_service.py""myapp.services.user_service"
"./services/user_service.py""services.user_service"
"myapp.services.user_service""myapp.services.user_service"
"""
if self.protocol != "function" or not self.url:
return ""
u = self.url.strip()
# 已经是点分模块路径(无路径分隔符且无 .py 后缀)
if re.match(r'^[\w]+(\.[\w]+)*$', u) and not u.endswith(".py"):
return u
# 文件路径 → 模块路径
u = u.replace("\\", "/")
u = re.sub(r'\.py$', '', u)
u = re.sub(r'^\.?/', '', u)
return u.replace("/", ".")
@property
def source_file(self) -> str:
"""function 协议的源文件路径(原始 url 字段)"""
return self.url if self.protocol == "function" else ""
# ══════════════════════════════════════════════════════════════
# 描述文件根结构
# ══════════════════════════════════════════════════════════════
@dataclass
class ApiDescriptor:
project: str
description: str
interfaces: list[InterfaceInfo]
# ══════════════════════════════════════════════════════════════
# 解析器
# ══════════════════════════════════════════════════════════════
class InterfaceParser:
# ── 公开接口 ──────────────────────────────────────────────
def parse_file(self, file_path: str) -> ApiDescriptor:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
return self.parse(data)
def parse(self, data: dict | list) -> ApiDescriptor:
"""
兼容三种格式:
新格式 {"project": "...", "description": "...", "units": [...]}
旧格式1{"project": "...", "units": [...]}
旧格式2[...] 直接是接口数组
"""
if isinstance(data, list):
return ApiDescriptor(
project="default",
description="",
interfaces=self._parse_units(data),
)
units = []
for unit in config.UNIT_KEYWORDS:
if unit not in data:
continue
units = data.get(unit, [])
return ApiDescriptor(
project=data.get("project", "default"),
description=data.get("description", ""),
interfaces=self._parse_units(units),
)
# ── 单元解析 ──────────────────────────────────────────────
def _parse_units(self, units: list[dict]) -> list[InterfaceInfo]:
result = []
for item in units:
protocol = (
item.get("protocol") or item.get("type") or "function"
).lower()
if protocol == "http":
result.append(self._parse_http(item))
else:
result.append(self._parse_function(item))
return result
def _parse_http(self, item: dict) -> InterfaceInfo:
"""
HTTP 接口解析。
参数统一从 "parameters" 字段读取,
同时兼容旧格式的 "request_parameters" / "url_parameters"
优先级parameters > request_parameters + url_parameters
"""
# 优先使用新统一字段 "parameters"
raw_params: dict = item.get("parameters", {})
# 旧格式兼容:合并 url_parameters + request_parameters
if not raw_params:
raw_params = {
**item.get("url_parameters", {}),
**item.get("request_parameters", {}),
}
return InterfaceInfo(
name=item["name"],
description=item.get("description", ""),
protocol="http",
url=item.get("url", ""),
requirement_id=item.get("requirement_id", ""),
method=item.get("method", "get").lower(),
parameters=self._parse_param_dict(raw_params, with_inout=True),
return_info=self._parse_return(item.get("return", {})),
)
def _parse_function(self, item: dict) -> InterfaceInfo:
return InterfaceInfo(
name=item["name"],
description=item.get("description", ""),
protocol="function",
url=item.get("url", ""),
requirement_id=item.get("requirement_id", ""),
parameters=self._parse_param_dict(
item.get("parameters", {}), with_inout=True,
),
return_info=self._parse_return(item.get("return", {})),
)
# ── 参数解析 ──────────────────────────────────────────────
def _parse_param_dict(
self,
params: dict,
with_inout: bool = False,
) -> list[ParameterInfo]:
result = []
for name, info in params.items():
result.append(ParameterInfo(
name=name,
type=info.get("type", "string"),
description=info.get("description", ""),
required=info.get("required", True),
default=info.get("default", None),
inout=info.get("inout", "in") if with_inout else "in",
))
return result
# ── 返回值解析 ────────────────────────────────────────────
def _parse_return(self, ret: dict) -> ReturnInfo:
if not ret:
return ReturnInfo()
return ReturnInfo(
type=ret.get("type", ""),
description=ret.get("description", ""),
on_success=self._parse_return_case(ret.get("on_success", {})),
on_failure=self._parse_return_case(ret.get("on_failure", {})),
)
def _parse_return_case(self, case: dict) -> ReturnCase:
if not case:
return ReturnCase()
return ReturnCase(
value=case.get("value"),
description=case.get("description", ""),
)
# ══════════════════════════════════════════════════════════
# 转换为 LLM 可读摘要
# ══════════════════════════════════════════════════════════
def to_summary_dict(self, interfaces: list[InterfaceInfo]) -> list[dict]:
return [
self._http_summary(i) if i.protocol == "http"
else self._function_summary(i)
for i in interfaces
]
def _http_summary(self, iface: InterfaceInfo) -> dict:
ri = iface.return_info
return {
"name": iface.name,
"requirement_id": iface.requirement_id,
"description": iface.description,
"protocol": "http",
"full_url": iface.http_full_url,
"method": iface.method,
# HTTP 接口统一用 "parameters" 输出给 LLM
"parameters": self._params_to_dict(iface.parameters),
"return": {
"type": ri.type,
"description": ri.description,
"on_success": {
"value": ri.on_success.value,
"description": ri.on_success.description,
},
"on_failure": {
"value": ri.on_failure.value,
"description": ri.on_failure.description,
},
},
}
def _function_summary(self, iface: InterfaceInfo) -> dict:
ri = iface.return_info
return {
"name": iface.name,
"requirement_id": iface.requirement_id,
"description": iface.description,
"protocol": "function",
"source_file": iface.source_file,
"module_path": iface.module_path,
"parameters": self._params_to_dict(iface.parameters),
"return": {
"type": ri.type,
"description": ri.description,
"on_success": {
"value": ri.on_success.value,
"description": ri.on_success.description,
},
"on_failure": {
"value": ri.on_failure.value,
"description": ri.on_failure.description,
},
},
}
def _params_to_dict(self, params: list[ParameterInfo]) -> dict:
result = {}
for p in params:
entry: dict = {
"type": p.type,
"inout": p.inout,
"description": p.description,
"required": p.required,
}
if p.default is not None:
entry["default"] = p.default
result[p.name] = entry
return result