You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
8.8 KiB
214 lines
8.8 KiB
|
2 days ago
|
# -*- coding: utf-8 -*-
|
||
|
|
"""票 04 · 嵌套 DTO 字段表展开 v2:按类声明切片解析,修静态嵌套类撞名/外层字段混入。
|
||
|
|
|
||
|
|
对「响应 data 结构」表里 list/object 形且类型可解析的字段,追加 `字段[].子字段` 点路径行,
|
||
|
|
深度 = 顶一层 + 嵌套一层。类解析:
|
||
|
|
- 全仓扫描每个 class/record/interface/enum 声明,花括号配对取类体;
|
||
|
|
- 同名多候选(如 ImportPreviewDTO.Row 与 ContactImportPreviewDTO.Row)→ 声明方同文件优先;
|
||
|
|
- 解析类体前剥除其内部嵌套类片段,避免外层/内层字段合并;
|
||
|
|
- extends 父类同样按此解析(同文件优先)。
|
||
|
|
默认 dry-run;--apply 才写回(只动「响应 data 结构」表,响应示例块原样保留,示例真值化归票 05)。
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "a4-doc-fields"))
|
||
|
|
import complete_a4_doc_fields as R # noqa: E402
|
||
|
|
|
||
|
|
NEST_PARENT_HINT = ",元素结构如下"
|
||
|
|
DECL_RE = re.compile(r'\b(class|record|interface|enum)\s+(\w+)')
|
||
|
|
|
||
|
|
|
||
|
|
def brace_span(text: str, start: int) -> tuple[int, int]:
|
||
|
|
"""text[start:] 应以 '{' 开头(跳过空白后),返回 (open_idx, close_idx)。"""
|
||
|
|
i = text.index('{', start)
|
||
|
|
depth = 0
|
||
|
|
for j in range(i, len(text)):
|
||
|
|
if text[j] == '{':
|
||
|
|
depth += 1
|
||
|
|
elif text[j] == '}':
|
||
|
|
depth -= 1
|
||
|
|
if depth == 0:
|
||
|
|
return i, j
|
||
|
|
raise ValueError("花括号不配对")
|
||
|
|
|
||
|
|
|
||
|
|
def type_bodies(sources: list[Path]) -> dict[str, list[tuple[Path, str]]]:
|
||
|
|
"""name → [(file, 含声明的类体切片)]。逐个声明独立切片(不跳过外层区间),嵌套类不漏。"""
|
||
|
|
out: dict[str, list[tuple[Path, str]]] = {}
|
||
|
|
for src in sources:
|
||
|
|
try:
|
||
|
|
text = src.read_text(encoding="utf-8-sig")
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
pos = 0
|
||
|
|
while True:
|
||
|
|
m = DECL_RE.search(text, pos)
|
||
|
|
if not m:
|
||
|
|
break
|
||
|
|
pos = m.end()
|
||
|
|
try:
|
||
|
|
o, c = brace_span(text, m.start())
|
||
|
|
except ValueError:
|
||
|
|
continue
|
||
|
|
out.setdefault(m.group(2), []).append((src, text[m.start():c + 1]))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def strip_nested(text: str) -> str:
|
||
|
|
"""剥除类体切片里第一个类声明之外的其他类片段(避免嵌套类字段混入外层解析)。"""
|
||
|
|
first = DECL_RE.search(text)
|
||
|
|
pos = first.end()
|
||
|
|
while True:
|
||
|
|
m = DECL_RE.search(text, pos)
|
||
|
|
if not m:
|
||
|
|
break
|
||
|
|
try:
|
||
|
|
o, c = brace_span(text, m.start())
|
||
|
|
except ValueError:
|
||
|
|
break
|
||
|
|
text = text[:m.start()] + text[c + 1:]
|
||
|
|
pos = m.start()
|
||
|
|
return text
|
||
|
|
|
||
|
|
|
||
|
|
def fields_of(name: str, bodies: dict[str, list[tuple[Path, str]]],
|
||
|
|
ctx: Path | None = None, seen: set[str] | None = None,
|
||
|
|
bindings: dict[str, str] | None = None) -> list[tuple[str, str, str]]:
|
||
|
|
"""解析名为 name 的类字段(含 extends 链与泛型绑定);ctx=声明方文件,同文件候选优先。
|
||
|
|
|
||
|
|
name 可带泛型实参(如 BatchResult<CustomerBatchFailItem>):取类声明的形式参数与实参
|
||
|
|
逐位绑定,替换到字段类型上(List<F> → List<CustomerBatchFailItem>)。
|
||
|
|
"""
|
||
|
|
seen = seen or set()
|
||
|
|
bindings = dict(bindings or {})
|
||
|
|
outer, generic_args = R.split_generic(name)
|
||
|
|
key = outer.split(".")[-1]
|
||
|
|
cands = bodies.get(key)
|
||
|
|
if not cands or key in seen:
|
||
|
|
return []
|
||
|
|
pick = cands[0]
|
||
|
|
for src, slice_ in cands:
|
||
|
|
if src == ctx:
|
||
|
|
pick = (src, slice_)
|
||
|
|
break
|
||
|
|
src, slice_ = pick
|
||
|
|
seen.add(key)
|
||
|
|
text = strip_nested(slice_)
|
||
|
|
# 类声明的形式参数 → 与调用方实参逐位绑定
|
||
|
|
pm = re.search(r'\b(?:class|record|interface)\s+\w+\s*<([^>]*)>', text)
|
||
|
|
if pm and generic_args:
|
||
|
|
params = [p.strip() for p in R.split_top_commas(pm.group(1))]
|
||
|
|
actuals = [a.strip() for a in R.split_top_commas(generic_args)]
|
||
|
|
for p, a in zip(params, actuals):
|
||
|
|
if p and a:
|
||
|
|
bindings[p] = a
|
||
|
|
rows = R.parse_record(text, key)
|
||
|
|
if rows is None:
|
||
|
|
rows = R.parse_class_fields(text)
|
||
|
|
|
||
|
|
def subst(t: str) -> str:
|
||
|
|
for p, a in bindings.items():
|
||
|
|
t = re.sub(r'\b' + re.escape(p) + r'\b', a, t)
|
||
|
|
return t
|
||
|
|
|
||
|
|
rows = [(n, subst(t), d) for n, t, d in rows]
|
||
|
|
pm2 = re.search(r'\bclass\s+\w+\s+extends\s+([\w.<>]+)', text)
|
||
|
|
if pm2:
|
||
|
|
rows = fields_of(pm2.group(1).split(".")[-1], bodies, src, seen, bindings) + rows
|
||
|
|
return rows
|
||
|
|
|
||
|
|
|
||
|
|
def nested_pass(rows: list[tuple[str, str, str]], item_name: str,
|
||
|
|
bodies: dict[str, list[tuple[Path, str]]],
|
||
|
|
item_src: Path | None, top_prefix: str = "") -> tuple[list[tuple[str, str, str]], list[str]]:
|
||
|
|
"""顶一层字段(带 top_prefix,page 形态为 content[].)+ 嵌套一层点路径子行。"""
|
||
|
|
out: list[tuple[str, str, str]] = []
|
||
|
|
missing: list[str] = []
|
||
|
|
top = fields_of(item_name, bodies, item_src)
|
||
|
|
for name, ftype, desc in top:
|
||
|
|
parent_desc = desc
|
||
|
|
shape, inner = R.type_shape(ftype)
|
||
|
|
expandable = shape in ("list", "object") and inner
|
||
|
|
sub: list[tuple[str, str, str]] = []
|
||
|
|
if expandable:
|
||
|
|
sub = fields_of(inner, bodies, item_src)
|
||
|
|
if sub and parent_desc and not parent_desc.endswith(NEST_PARENT_HINT):
|
||
|
|
parent_desc += NEST_PARENT_HINT
|
||
|
|
out.append((top_prefix + name, ftype, parent_desc))
|
||
|
|
if sub:
|
||
|
|
base = f"{top_prefix}{name}"
|
||
|
|
prefix = f"{base}[]." if shape == "list" else f"{base}."
|
||
|
|
for sname, stype, sdesc in sub:
|
||
|
|
if not sdesc:
|
||
|
|
missing.append(f"{inner.split('.')[-1]}.{sname}")
|
||
|
|
out.append((prefix + sname, stype, sdesc))
|
||
|
|
return out, missing
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--apply", action="store_true")
|
||
|
|
parser.add_argument("--from-list", help="文件清单(每行一个 .bru 相对路径)")
|
||
|
|
parser.add_argument("files", nargs="*", help="限定 .bru 文件相对路径(默认扫全仓嵌套引用)")
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
sources = R.java_sources()
|
||
|
|
endpoints = R.endpoint_index(sources)
|
||
|
|
bodies = type_bodies(sources)
|
||
|
|
|
||
|
|
if args.from_list:
|
||
|
|
targets = [R.DOCS / l.strip() for l in open(args.from_list, encoding="utf-8") if l.strip().endswith(".bru")]
|
||
|
|
elif args.files:
|
||
|
|
targets = [R.DOCS / f for f in args.files]
|
||
|
|
else:
|
||
|
|
targets = [f for f in sorted(R.DOCS.rglob("*.bru")) if f.name != "folder.bru"
|
||
|
|
and re.search(r"^\s*\| [a-zA-Z\[\].]+ \| (?:List<)?[A-Z][\w.]*", f.read_text(encoding="utf-8-sig"), re.M)]
|
||
|
|
|
||
|
|
changed = 0
|
||
|
|
for file in targets:
|
||
|
|
text = file.read_text(encoding="utf-8-sig")
|
||
|
|
key = R.endpoint_key(text)
|
||
|
|
if not key or key not in endpoints:
|
||
|
|
print(f"SKIP 无对账钥匙或未映射: {file.relative_to(R.DOCS)}")
|
||
|
|
continue
|
||
|
|
ep = endpoints[key]
|
||
|
|
shape, item = R.type_shape(ep.return_type)
|
||
|
|
if shape in ("void", "binary", "scalar") or not item:
|
||
|
|
continue
|
||
|
|
it = item.split(".")[-1]
|
||
|
|
rows, missing = nested_pass([], it, bodies, ep.source, top_prefix="content[]." if shape == "page" else "")
|
||
|
|
if not rows:
|
||
|
|
print(f"SKIP 顶层类型未解析出字段(防清空表,不写回): {file.relative_to(R.DOCS)} item={item}")
|
||
|
|
continue
|
||
|
|
if shape == "page":
|
||
|
|
rows = [("content[]", "array", "分页内容,元素结构如下")] + rows + [
|
||
|
|
("total", "Long", "总条数"), ("size", "Long", "每页条数"),
|
||
|
|
("current", "Long", "当前页"), ("pages", "Long", "总页数"), ("empty", "Boolean", "是否为空")]
|
||
|
|
old_rows, _, _ = R.data_structure(ep.return_type, {})
|
||
|
|
if rows == old_rows:
|
||
|
|
continue
|
||
|
|
rel = file.relative_to(R.DOCS).as_posix()
|
||
|
|
old_names = [r[0] for r in old_rows]
|
||
|
|
added = [r for r in rows if r[0] not in old_names]
|
||
|
|
print(f"{'WRITE' if args.apply else 'DRY '} {rel} +{len(added)} 行 缺@Schema={missing or '无'}")
|
||
|
|
for n, t, d in added:
|
||
|
|
print(f" | {n} | {t} | {d} |")
|
||
|
|
if args.apply:
|
||
|
|
table_only = "\n".join(R.section(rows, None, keep_sample_hint=False).split("\n\n ## 响应示例", 1)[0].splitlines())
|
||
|
|
pattern = re.compile(r" ## 响应 data 结构[\s\S]*?(?=\n ## 响应示例)")
|
||
|
|
new_text, count = pattern.subn(table_only, text, count=1)
|
||
|
|
if count != 1:
|
||
|
|
raise SystemExit(f"未找到唯一响应区块: {rel}")
|
||
|
|
file.write_text(new_text, encoding="utf-8", newline="")
|
||
|
|
changed += 1
|
||
|
|
print(f"changed={changed} / targets={len(targets)}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|