check_pl_json_aliases.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. #!/usr/bin/env python3
  2. """校验运输计划模块 Pydantic Schema 是否具备 camelCase alias(防 422)。
  3. 用法(在 backend 目录):
  4. ./venv/bin/python scripts/check_pl_json_aliases.py
  5. """
  6. from __future__ import annotations
  7. import ast
  8. import re
  9. import sys
  10. from pathlib import Path
  11. SCHEMA_FILES = [
  12. "app/schemas/pl/base_data.py",
  13. "app/schemas/pl/plan.py",
  14. "app/schemas/pl/transport.py",
  15. "app/schemas/pl/freight.py",
  16. "app/schemas/pl/inventory.py",
  17. ]
  18. # 单段 snake_case 公共字段可不加 alias(JSON 与 Python 同名)
  19. SINGLE_WORD_OK = {
  20. "id",
  21. "q",
  22. "page",
  23. "status",
  24. "remark",
  25. "model",
  26. "unit",
  27. "action",
  28. "comment",
  29. "total",
  30. "list",
  31. "products",
  32. "transports",
  33. "period",
  34. "qty",
  35. "amount",
  36. }
  37. def snake_to_camel(name: str) -> str:
  38. parts = name.split("_")
  39. return parts[0] + "".join(p.capitalize() for p in parts[1:])
  40. def iter_field_assignments(tree: ast.AST):
  41. for node in ast.walk(tree):
  42. if not isinstance(node, ast.ClassDef):
  43. continue
  44. for stmt in node.body:
  45. if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
  46. yield node.name, stmt.target.id, stmt.value
  47. elif isinstance(stmt, ast.Assign):
  48. for target in stmt.targets:
  49. if isinstance(target, ast.Name):
  50. yield node.name, target.id, stmt.value
  51. def field_has_alias(value: ast.expr | None) -> bool:
  52. if value is None:
  53. return False
  54. if isinstance(value, ast.Call):
  55. for kw in value.keywords:
  56. if kw.arg == "alias":
  57. return True
  58. return False
  59. def main() -> int:
  60. root = Path(__file__).resolve().parents[1]
  61. errors: list[str] = []
  62. for rel in SCHEMA_FILES:
  63. path = root / rel
  64. if not path.exists():
  65. errors.append(f"缺少文件: {rel}")
  66. continue
  67. source = path.read_text(encoding="utf-8")
  68. tree = ast.parse(source, filename=str(path))
  69. for class_name, field_name, value in iter_field_assignments(tree):
  70. if "_" not in field_name:
  71. continue
  72. if field_name in SINGLE_WORD_OK or field_name == "model_config":
  73. continue
  74. if not field_has_alias(value):
  75. expected = snake_to_camel(field_name)
  76. errors.append(
  77. f"{rel}: {class_name}.{field_name} 缺少 Field(alias=\"{expected}\")"
  78. )
  79. if errors:
  80. print("PL JSON alias 校验失败(可能导致前端 camelCase 请求 422):\n")
  81. for e in errors:
  82. print(f" - {e}")
  83. print("\n规范: openspec/changes/transport-plan-management/specs/pl-api-json/spec.md")
  84. return 1
  85. print("PL JSON alias 校验通过。")
  86. return 0
  87. if __name__ == "__main__":
  88. sys.exit(main())