test
Some checks failed
API Parameter Change Check / api-param-check (push) Failing after 3s

This commit is contained in:
2026-06-03 15:02:21 +08:00
parent 2eacae577c
commit 556f5b8ab6
25 changed files with 2119 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
"""
API 参数变更对比模块。
对比新旧两个版本的 Controller 端点,识别参数的增、删、改、重命名。
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Set, Tuple
from controller_parser import ApiEndpoint, ApiParameter
class ChangeType(str, Enum):
"""参数变更类型。"""
ADDED = "added"
REMOVED = "removed"
MODIFIED = "modified"
RENAMED = "renamed"
@dataclass
class ParameterChange:
"""单条参数变更记录。"""
change_type: ChangeType
param_name: str
param_type: Optional[str] = None
old_name: Optional[str] = None
old_type: Optional[str] = None
required: Optional[bool] = None
old_required: Optional[bool] = None
detail: Optional[str] = None
def to_display_line(self) -> str:
"""
格式化为通知模板中的一行文本。
:return: 如 "删除: Boolean userType""重命名: String userName -> String accountName"
"""
if self.change_type == ChangeType.REMOVED:
return f" - 删除: {self.param_type} {self.param_name}"
if self.change_type == ChangeType.ADDED:
req_text = f" (是否必填:{str(self.required).lower()})" if self.required is not None else ""
return f" - 新增: {self.param_type} {self.param_name}{req_text}"
if self.change_type == ChangeType.RENAMED:
return f" - 重命名: {self.old_type} {self.old_name} -> {self.param_type} {self.param_name}"
if self.change_type == ChangeType.MODIFIED:
parts = [f" - 修改: {self.param_name}"]
if self.detail:
parts.append(f" ({self.detail})")
return "".join(parts)
return f" - {self.change_type.value}: {self.param_name}"
@dataclass
class EndpointChangeReport:
"""单个接口的变更报告。"""
uri: str
http_method: str
controller_class: str
method_name: str
parameter_changes: List[ParameterChange] = field(default_factory=list)
is_new_endpoint: bool = False
is_removed_endpoint: bool = False
@property
def has_changes(self) -> bool:
"""是否存在任何变更。"""
return (
self.is_new_endpoint
or self.is_removed_endpoint
or len(self.parameter_changes) > 0
)
@property
def endpoint_key(self) -> str:
return f"{self.http_method} {self.uri}"
def _param_key(p: ApiParameter) -> Tuple[str, str]:
"""参数匹配键:(source, name)。"""
return (p.source, p.name)
def compare_parameters(
old_params: List[ApiParameter], new_params: List[ApiParameter]
) -> List[ParameterChange]:
"""
对比同一接口新旧版本的参数列表,识别增删改及重命名。
重命名启发式:若删除与新增参数类型相同且 source 相同,则视为重命名。
:param old_params: 旧版本参数
:param new_params: 新版本参数
:return: 变更列表
"""
changes: List[ParameterChange] = []
old_map: Dict[Tuple[str, str], ApiParameter] = {_param_key(p): p for p in old_params}
new_map: Dict[Tuple[str, str], ApiParameter] = {_param_key(p): p for p in new_params}
old_keys = set(old_map.keys())
new_keys = set(new_map.keys())
removed_keys = old_keys - new_keys
added_keys = new_keys - old_keys
common_keys = old_keys & new_keys
# 1. 共同参数:检查类型、必填等属性变更
for key in common_keys:
old_p = old_map[key]
new_p = new_map[key]
detail_parts = []
if old_p.type != new_p.type:
detail_parts.append(f"类型 {old_p.type} -> {new_p.type}")
if old_p.required != new_p.required:
detail_parts.append(f"必填 {old_p.required} -> {new_p.required}")
if detail_parts:
changes.append(
ParameterChange(
change_type=ChangeType.MODIFIED,
param_name=new_p.name,
param_type=new_p.type,
required=new_p.required,
old_required=old_p.required,
detail=", ".join(detail_parts),
)
)
# 2. 重命名检测:在 removed + added 中找同 type + source 的配对
unmatched_removed: List[Tuple[Tuple[str, str], ApiParameter]] = []
unmatched_added: List[Tuple[Tuple[str, str], ApiParameter]] = []
for key in removed_keys:
unmatched_removed.append((key, old_map[key]))
for key in added_keys:
unmatched_added.append((key, new_map[key]))
matched_removed: Set[Tuple[str, str]] = set()
matched_added: Set[Tuple[str, str]] = set()
for r_key, r_param in unmatched_removed:
for a_key, a_param in unmatched_added:
if a_key in matched_added:
continue
if r_param.type == a_param.type and r_param.source == a_param.source:
changes.append(
ParameterChange(
change_type=ChangeType.RENAMED,
param_name=a_param.name,
param_type=a_param.type,
old_name=r_param.name,
old_type=r_param.type,
required=a_param.required,
)
)
matched_removed.add(r_key)
matched_added.add(a_key)
break
# 3. 纯删除
for key, param in unmatched_removed:
if key not in matched_removed:
changes.append(
ParameterChange(
change_type=ChangeType.REMOVED,
param_name=param.name,
param_type=param.type,
)
)
# 4. 纯新增
for key, param in unmatched_added:
if key not in matched_added:
changes.append(
ParameterChange(
change_type=ChangeType.ADDED,
param_name=param.name,
param_type=param.type,
required=param.required,
)
)
return changes
def compare_endpoints(
old_endpoints: Dict[str, ApiEndpoint],
new_endpoints: Dict[str, ApiEndpoint],
) -> List[EndpointChangeReport]:
"""
对比新旧两个版本的全部 Controller 端点,生成变更报告列表。
:param old_endpoints: 旧版本 { endpoint_key: ApiEndpoint }
:param new_endpoints: 新版本 { endpoint_key: ApiEndpoint }
:return: 有变更的接口报告列表
"""
reports: List[EndpointChangeReport] = []
all_keys = set(old_endpoints.keys()) | set(new_endpoints.keys())
for key in sorted(all_keys):
old_ep = old_endpoints.get(key)
new_ep = new_endpoints.get(key)
if old_ep is None and new_ep is not None:
# 全新接口
reports.append(
EndpointChangeReport(
uri=new_ep.uri,
http_method=new_ep.http_method,
controller_class=new_ep.controller_class,
method_name=new_ep.method_name,
is_new_endpoint=True,
parameter_changes=[
ParameterChange(
change_type=ChangeType.ADDED,
param_name=p.name,
param_type=p.type,
required=p.required,
)
for p in new_ep.parameters
],
)
)
continue
if new_ep is None and old_ep is not None:
# 接口被删除
reports.append(
EndpointChangeReport(
uri=old_ep.uri,
http_method=old_ep.http_method,
controller_class=old_ep.controller_class,
method_name=old_ep.method_name,
is_removed_endpoint=True,
)
)
continue
# 同 URI 对比参数
param_changes = compare_parameters(old_ep.parameters, new_ep.parameters)
if param_changes:
reports.append(
EndpointChangeReport(
uri=new_ep.uri,
http_method=new_ep.http_method,
controller_class=new_ep.controller_class,
method_name=new_ep.method_name,
parameter_changes=param_changes,
)
)
return [r for r in reports if r.has_changes]