106 lines
2.8 KiB
Python
106 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
||
"""校验运输计划模块 Pydantic Schema 是否具备 camelCase alias(防 422)。
|
||
|
||
用法(在 backend 目录):
|
||
./venv/bin/python scripts/check_pl_json_aliases.py
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
SCHEMA_FILES = [
|
||
"app/schemas/pl/base_data.py",
|
||
"app/schemas/pl/plan.py",
|
||
"app/schemas/pl/transport.py",
|
||
"app/schemas/pl/freight.py",
|
||
"app/schemas/pl/inventory.py",
|
||
]
|
||
|
||
# 单段 snake_case 公共字段可不加 alias(JSON 与 Python 同名)
|
||
SINGLE_WORD_OK = {
|
||
"id",
|
||
"q",
|
||
"page",
|
||
"status",
|
||
"remark",
|
||
"model",
|
||
"unit",
|
||
"action",
|
||
"comment",
|
||
"total",
|
||
"list",
|
||
"products",
|
||
"transports",
|
||
"period",
|
||
"qty",
|
||
"amount",
|
||
}
|
||
|
||
|
||
def snake_to_camel(name: str) -> str:
|
||
parts = name.split("_")
|
||
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||
|
||
|
||
def iter_field_assignments(tree: ast.AST):
|
||
for node in ast.walk(tree):
|
||
if not isinstance(node, ast.ClassDef):
|
||
continue
|
||
for stmt in node.body:
|
||
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
|
||
yield node.name, stmt.target.id, stmt.value
|
||
elif isinstance(stmt, ast.Assign):
|
||
for target in stmt.targets:
|
||
if isinstance(target, ast.Name):
|
||
yield node.name, target.id, stmt.value
|
||
|
||
|
||
def field_has_alias(value: ast.expr | None) -> bool:
|
||
if value is None:
|
||
return False
|
||
if isinstance(value, ast.Call):
|
||
for kw in value.keywords:
|
||
if kw.arg == "alias":
|
||
return True
|
||
return False
|
||
|
||
|
||
def main() -> int:
|
||
root = Path(__file__).resolve().parents[1]
|
||
errors: list[str] = []
|
||
|
||
for rel in SCHEMA_FILES:
|
||
path = root / rel
|
||
if not path.exists():
|
||
errors.append(f"缺少文件: {rel}")
|
||
continue
|
||
source = path.read_text(encoding="utf-8")
|
||
tree = ast.parse(source, filename=str(path))
|
||
for class_name, field_name, value in iter_field_assignments(tree):
|
||
if "_" not in field_name:
|
||
continue
|
||
if field_name in SINGLE_WORD_OK or field_name == "model_config":
|
||
continue
|
||
if not field_has_alias(value):
|
||
expected = snake_to_camel(field_name)
|
||
errors.append(
|
||
f"{rel}: {class_name}.{field_name} 缺少 Field(alias=\"{expected}\")"
|
||
)
|
||
|
||
if errors:
|
||
print("PL JSON alias 校验失败(可能导致前端 camelCase 请求 422):\n")
|
||
for e in errors:
|
||
print(f" - {e}")
|
||
print("\n规范: openspec/changes/transport-plan-management/specs/pl-api-json/spec.md")
|
||
return 1
|
||
|
||
print("PL JSON alias 校验通过。")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|