from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ALLOWLIST = {
"unwrap", "expect", "unwrap_err", "unwrap_or", "unwrap_or_else",
"unwrap_or_default", "map_err", "map", "and_then", "ok_or_else", "clone",
"to_string", "into", "iter", "collect", "push", "insert", "get", "len",
"await", "as_str", "as_ref", "parse", "join", "spawn", "lock", "read",
"write", "send", "recv", "next", "filter", "for_each", "unwrap_or_err",
"run", "serve", "bind", "block_on", "sleep", "timeout", "abort",
"route", "layer", "with_state", "into_make_service", "nest",
"install_default", "load", "add_parsable_certificates", "with_root_certificates",
"with_no_client_auth", "with_safe_defaults", "builder",
"to_owned", "from", "try_into", "deserialize", "serialize",
}
def crate_symbols() -> set[str]:
src = "\n".join(p.read_text() for p in ROOT.glob("src/**/*.rs"))
names: set[str] = set()
for pattern in (
r"\bpub(?:\([^)]*\))?\s+(?:struct|enum|trait|type|const|static)\s+(\w+)",
r"\bpub(?:\([^)]*\))?\s+(?:const\s+)?(?:async\s+)?fn\s+(\w+)",
r"pub use [^;]*?\b(\w+)\s*(?:,|\}|;|\s+as\b)",
):
names.update(re.findall(pattern, src))
names.update(re.findall(r"^\s{4}(\w+)\s*(?:=\s*-?\d+)?\s*,\s*$", src, re.M))
return names
def main() -> int:
defined = crate_symbols() | ALLOWLIST
failures: list[tuple[str, str, int]] = []
scanned = 0
for doc in sorted(ROOT.glob("site/content/**/*.md")):
text = doc.read_text()
scanned += 1
rel = str(doc.relative_to(ROOT))
for m in re.finditer(r"\bkrafka::(?:[a-z_]+::)*([A-Z]\w+)", text):
if m.group(1) not in defined:
failures.append((rel, m.group(1), text[: m.start()].count("\n") + 1))
for m in re.finditer(r"^\s*\.(\w+)\(", text, re.M):
if m.group(1) not in defined:
failures.append((rel, m.group(1), text[: m.start()].count("\n") + 1))
if failures:
print("Documentation API check FAILED\n", file=sys.stderr)
seen = set()
for rel, name, line in failures:
if (rel, name) in seen:
continue
seen.add((rel, name))
print(
f" - {rel}:{line} `{name}` is not defined by the crate.\n"
" Either the guide names a renamed or removed API, or the name "
"belongs to\n another crate and should be added to ALLOWLIST in "
"xtask/doc_api.py.\n",
file=sys.stderr,
)
return 1
print(f"✓ Documentation API: {scanned} files scanned, every krafka name resolves")
return 0
if __name__ == "__main__":
sys.exit(main())