"""生成 libmcpp 资源协作接口 API 文档。
功能:从 libmcpp 生产源码的 MC_INTERFACE/MC_REFLECT 自动发现接口,以实现侧
成员、类型和读写权限为文档主体,并使用匹配的 mdb_interface 定义补充说明、
取值范围和复杂类型定义。
输入:mdb_interface 仓库路径。
输出:docs/api/mdb_interfaces.md。
执行:在 libmcpp 根目录运行 `python scripts/generate_mdb_api_docs.py
--mdb-root ../mdb_interface`。
退出码:普通生成模式以文档是否成功写出为准;--check 模式在文档需要
重新生成、无法读取或扫描存在警告时返回 1。接口差异由一致性比较工具负责判定。
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from mdb_api_common import (
Interface,
Method,
Parameter,
Property,
definition_reference,
load_implementation_interfaces,
load_mdb_interfaces,
markdown_text,
value_range,
)
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_OUTPUT = ROOT / "docs" / "api" / "mdb_interfaces.md"
def partition_interfaces(
implementation: dict[str, Interface], mdb: dict[str, Interface]
) -> tuple[dict[str, Interface], list[str]]:
"""按是否存在 MDB 定义拆分 libmcpp 实现接口。"""
comparable = {
name: interface for name, interface in implementation.items() if name in mdb
}
missing_mdb = sorted(set(implementation) - set(mdb))
return comparable, missing_mdb
def _casefold_member(mapping: dict, name: str):
"""精确匹配成员名,或在大小写无歧义时匹配同名成员。"""
if name in mapping:
return mapping[name]
matches = [value for key, value in mapping.items() if key.casefold() == name.casefold()]
return matches[0] if len(matches) == 1 else None
def _property_view(actual: Property, expected: Property | None) -> Property:
"""以实现属性为主体,使用 MDB 补充说明和约束。"""
return Property(
name=actual.name,
type_name=actual.type_name,
access=actual.access,
value_range=expected.value_range if expected is not None else "NA",
description=expected.description if expected is not None else "NA(MDB 未定义)",
source_member=actual.source_member,
)
def _parameter_view(
actual: Parameter,
expected_parameters: list[Parameter],
index: int,
) -> Parameter:
"""以实现参数类型为主体,按位置补充 MDB 参数元数据。"""
expected = expected_parameters[index] if index < len(expected_parameters) else None
return Parameter(
name=expected.name if expected is not None else actual.name,
type_name=actual.type_name,
description=(
expected.description
if expected is not None and expected.description
else "NA(MDB 未提供参数说明)"
),
value_range=expected.value_range if expected is not None else "NA",
reference=expected.reference if expected is not None else "",
)
def _method_view(actual: Method, expected: Method | None) -> Method:
"""以实现方法签名为主体,使用 MDB 补充名称和说明。"""
expected_inputs = expected.inputs if expected is not None else []
expected_outputs = expected.outputs if expected is not None else []
return Method(
name=actual.name,
inputs=[
_parameter_view(parameter, expected_inputs, index)
for index, parameter in enumerate(actual.inputs)
],
outputs=[
_parameter_view(parameter, expected_outputs, index)
for index, parameter in enumerate(actual.outputs)
],
description=(
expected.description
if expected is not None and expected.description
else "NA(MDB 未提供方法说明)"
),
source_member=actual.source_member,
)
def _parameter_summary(parameters: list[Parameter]) -> str:
if not parameters:
return "NA"
return "<br>".join(
f"`{markdown_text(parameter.name)}: {markdown_text(parameter.type_name)}`"
for parameter in parameters
)
def _render_properties(
implementation: Interface, expected: Interface | None
) -> list[str]:
"""渲染实现侧存在的属性;接口没有属性时不生成本节。"""
if not implementation.properties:
return []
lines = [
"### 属性",
"",
"| 属性名 | 类型 | 读写 | 取值范围 | 说明 |",
"| ------ | ---- | ---- | -------- | ---- |",
]
expected_properties = expected.properties if expected is not None else {}
for actual in implementation.properties.values():
metadata = _casefold_member(expected_properties, actual.name)
prop = _property_view(actual, metadata)
lines.append(
f"| {markdown_text(prop.name)} | {markdown_text(prop.type_name)} | "
f"{prop.access} | {markdown_text(prop.value_range)} | "
f"{markdown_text(prop.description)} |"
)
return lines
def _render_parameter_table(title: str, parameters: list[Parameter]) -> list[str]:
if not parameters:
return []
lines = [
f"##### {title}",
"",
"| 参数名 | 类型 | 引用类型 | 取值范围 | 说明 |",
"| ------ | ---- | -------- | -------- | ---- |",
]
for parameter in parameters:
lines.append(
f"| {markdown_text(parameter.name)} | {markdown_text(parameter.type_name)} | "
f"{markdown_text(parameter.reference) or 'NA'} | "
f"{markdown_text(parameter.value_range)} | "
f"{markdown_text(parameter.description)} |"
)
return lines
def _render_complex_type(
reference: str,
definitions: dict,
rendered: set[str],
) -> tuple[list[str], list[str]]:
"""展开单个 MDB 引用类型,并返回其中继续引用的类型。"""
if not reference or reference in rendered:
return [], []
rendered.add(reference)
definition = definitions.get(reference)
if not isinstance(definition, dict):
return [
f"##### 复杂类型 `{markdown_text(reference)}`",
"",
"MDB 中未找到该引用类型的字段定义。",
], []
metadata_keys = {"$comment", "description", "title"}
enum_items = [
(name, item) for name, item in definition.items() if name not in metadata_keys
]
enum_types = {type(item) for _, item in enum_items}
is_enum = (
bool(enum_items)
and len(enum_types) == 1
and enum_types.pop() in {bool, float, int, str}
)
if is_enum:
lines = [
f"##### 复杂类型 `{markdown_text(reference)}`(枚举)",
"",
"| 名称 | 值 |",
"| ---- | -- |",
]
for name, item in enum_items:
lines.append(f"| {markdown_text(name)} | {markdown_text(str(item))} |")
return lines, []
lines = [
f"##### 复杂类型 `{markdown_text(reference)}`(结构)",
"",
"| 字段名 | 类型 | 引用类型 | 取值范围 | 说明 |",
"| ------ | ---- | -------- | -------- | ---- |",
]
nested_references = []
for field_name, field_definition in definition.items():
if not isinstance(field_definition, dict):
continue
nested_reference = definition_reference(field_definition)
if nested_reference:
nested_references.append(nested_reference)
lines.append(
f"| {markdown_text(field_name)} | "
f"{markdown_text(field_definition.get('baseType', 'unknown'))} | "
f"{markdown_text(nested_reference) or 'NA'} | "
f"{markdown_text(value_range(field_definition))} | "
f"{markdown_text(field_definition.get('description', 'NA'))} |"
)
return lines, nested_references
def _render_complex_types(
parameters: list[Parameter],
definitions: dict,
rendered: set[str],
) -> list[str]:
references = [parameter.reference for parameter in parameters if parameter.reference]
lines = []
while references:
reference = references.pop(0)
section, nested = _render_complex_type(reference, definitions, rendered)
if section:
if lines:
lines.append("")
lines.extend(section)
references.extend(nested)
return lines
def _render_methods(
implementation: Interface, expected: Interface | None
) -> list[str]:
"""渲染实现侧存在的方法;接口没有方法时不生成本节。"""
if not implementation.methods:
return []
expected_methods = expected.methods if expected is not None else {}
methods = [
_method_view(actual, _casefold_member(expected_methods, actual.name))
for actual in implementation.methods.values()
]
lines = [
"### 方法",
"",
"| 方法名 | 入参 | 返回值 | 说明 |",
"| ------ | ---- | ------ | ---- |",
]
for method in methods:
lines.append(
f"| {markdown_text(method.name)} | {_parameter_summary(method.inputs)} | "
f"{_parameter_summary(method.outputs)} | {markdown_text(method.description)} |"
)
definitions = expected.definitions if expected is not None else {}
for method in methods:
lines.extend(["", f"#### {method.name}", "", markdown_text(method.description)])
input_table = _render_parameter_table("入参", method.inputs)
if input_table:
lines.extend(["", *input_table])
output_table = _render_parameter_table("返回值", method.outputs)
if output_table:
lines.extend(["", *output_table])
complex_types = _render_complex_types(
[*method.inputs, *method.outputs], definitions, set()
)
if complex_types:
lines.extend(["", *complex_types])
return lines
def render_document(
implementation: dict[str, Interface],
mdb: dict[str, Interface],
) -> str:
"""以 libmcpp 实现能力为主体渲染完整 Markdown 文档。"""
lines = [
"# libmcpp 资源协作接口 API",
"",
"<!-- 本文件由 scripts/generate_mdb_api_docs.py 生成,请勿手工修改。 -->",
"",
"本文档以 libmcpp 生产源码中通过 `MC_INTERFACE` 和 `MC_REFLECT` "
"实际暴露的接口能力为准。接口、属性、方法、类型和读写权限来自 libmcpp;"
"匹配的 mdb_interface 定义用于补充说明、取值范围和复杂类型字段。"
"实现与标准定义不一致的内容在一致性比较报告中呈现。",
"",
"- 属性读写:根据 libmcpp 反射只读标记生成 RO、RW 或 NA",
"- 取值范围和说明:取自匹配的 mdb_interface 定义;无定义时为 NA",
"",
"## 接口概览",
"",
"| 接口名 | libmcpp 类 | 实现位置 | 标准接口 | MDB 定义位置 |",
"| ------ | ----------- | -------- | -------- | ------------ |",
]
for name, interface in implementation.items():
expected = mdb.get(name)
mdb_path = f"`{expected.source_file}`" if expected is not None else "NA"
lines.append(
f"| `{name}` | `{interface.class_name}` | `{interface.source_file}` | "
f"{'是' if expected is not None else '否'} | {mdb_path} |"
)
for name, interface in implementation.items():
expected = mdb.get(name)
lines.extend(
[
"",
f"## {name}",
"",
f"- libmcpp 类:`{interface.class_name}`",
f"- 实现位置:`{interface.source_file}`",
(
f"- MDB 定义:`{expected.source_file}`"
if expected is not None
else "- MDB 定义:NA(非标准接口)"
),
]
)
properties = _render_properties(interface, expected)
if properties:
lines.extend(["", *properties])
methods = _render_methods(interface, expected)
if methods:
lines.extend(["", *methods])
return "\n".join(lines).rstrip() + "\n"
def _print_missing_mdb(missing_mdb: list[str]) -> None:
"""把缺少 MDB 定义的实现接口打印到标准错误。"""
if not missing_mdb:
return
print("以下 libmcpp 实现接口未在 mdb_interface 中定义:", file=sys.stderr)
for name in missing_mdb:
print(f" - {name}", file=sys.stderr)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--mdb-root", required=True, type=Path, help="mdb_interface 仓库根目录")
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT, help="Markdown 输出文件")
parser.add_argument("--check", action="store_true", help="只检查现有文档是否需要重新生成")
args = parser.parse_args()
mdb_root = args.mdb_root.resolve()
scan_warnings: list[str] = []
implementation = load_implementation_interfaces(ROOT, scan_warnings)
try:
mdb = load_mdb_interfaces(mdb_root, scan_warnings)
except FileNotFoundError as error:
print(f"无法加载 MDB 定义:{error}", file=sys.stderr)
print(
"请通过 --mdb-root 指定 mdb_interface 仓库根目录。",
file=sys.stderr,
)
return 1
_, missing_mdb = partition_interfaces(implementation, mdb)
content = render_document(implementation, mdb)
output = args.output.resolve()
if scan_warnings:
print("扫描 libmcpp 源码或 MDB 定义时发现以下警告:", file=sys.stderr)
for warning in scan_warnings:
print(f" - {warning}", file=sys.stderr)
if args.check:
if not output.is_file():
print(f"文档需要重新生成: {output}", file=sys.stderr)
return 1
try:
actual_content = output.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as error:
print(f"无法读取现有文档 {output}: {error}", file=sys.stderr)
return 1
if actual_content != content:
print(f"文档需要重新生成: {output}", file=sys.stderr)
return 1
_print_missing_mdb(missing_mdb)
if scan_warnings:
print(
"文档内容与可解析结果一致,但存在扫描警告,无法确认文档完整性。",
file=sys.stderr,
)
return 1
print(f"文档已是最新: {output}")
return 0
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(content, encoding="utf-8", newline="\n")
print(f"已生成 {len(implementation)} 个接口的文档: {output}")
for name in implementation:
print(f" - {name}")
_print_missing_mdb(missing_mdb)
if missing_mdb or scan_warnings:
print(
"文档已成功写出;上述完整性问题请使用一致性比较工具处理。",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())