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",
"as_millis", "as_secs", "duration_since", "tick", "extend", "is_empty",
"as_deref", "unwrap_or_default", "from_utf8_lossy", "retain", "contains",
"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",
"query_one", "execute",
}
REMOVED = {
"max_in_flight",
}
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
removed_pattern = re.compile(
r"\b(" + "|".join(sorted(map(re.escape, REMOVED))) + r")\b"
)
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))
for m in removed_pattern.finditer(text):
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))
if name in REMOVED:
print(
f" - {rel}:{line} `{name}` was removed from the public API.\n"
" Rewrite the passage; see REMOVED in xtask/doc_api.py for what\n"
" replaced it.\n",
file=sys.stderr,
)
else:
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())