from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PAIRS = [
(
("ConsumerBuilder", "src/consumer/builder.rs"),
("ConsumerConfigBuilder", "src/consumer/config.rs"),
),
(
("ProducerBuilder", "src/producer/mod.rs"),
("ProducerConfigBuilder", "src/producer/config.rs"),
),
(
("AdminClientBuilder", "src/admin/builder.rs"),
("AdminConfigBuilder", "src/admin/mod.rs"),
),
]
NOT_OPTIONS = {
"build",
"builder",
"new",
"default",
"pool",
"owns_pool",
"connection_metrics",
"is_closed",
"update_seed_brokers",
}
def impl_block(source: str, type_name: str) -> str:
stripped = strip_literals_and_comments(source)
match = re.search(rf"\bimpl\s+{re.escape(type_name)}\s*(?:<[^>]*>)?\s*\{{", stripped)
if not match:
raise SystemExit(f"could not find `impl {type_name}`")
start = match.end() - 1
depth = 0
for i in range(start, len(stripped)):
if stripped[i] == "{":
depth += 1
elif stripped[i] == "}":
depth -= 1
if depth == 0:
return stripped[start + 1 : i]
raise SystemExit(f"unbalanced braces in `impl {type_name}`")
def strip_literals_and_comments(source: str) -> str:
out = list(source)
i = 0
n = len(source)
while i < n:
ch = source[i]
if ch == "/" and i + 1 < n and source[i + 1] == "/":
while i < n and source[i] != "\n":
out[i] = " "
i += 1
elif ch == "/" and i + 1 < n and source[i + 1] == "*":
depth = 1
out[i] = out[i + 1] = " "
i += 2
while i < n and depth:
if source.startswith("/*", i):
depth += 1
out[i] = out[i + 1] = " "
i += 2
elif source.startswith("*/", i):
depth -= 1
out[i] = out[i + 1] = " "
i += 2
else:
out[i] = " " if source[i] != "\n" else "\n"
i += 1
elif ch == '"':
out[i] = " "
i += 1
while i < n:
if source[i] == "\\":
out[i] = " "
if i + 1 < n:
out[i + 1] = " "
i += 2
continue
if source[i] == '"':
out[i] = " "
i += 1
break
out[i] = " " if source[i] != "\n" else "\n"
i += 1
else:
i += 1
return "".join(out)
def setters(body: str) -> set[str]:
return {m.group(1) for m in re.finditer(r"\bpub\s+fn\s+(\w+)", body)}
def main() -> int:
failures: list[str] = []
for (public_name, public_path), (config_name, config_path) in PAIRS:
public = setters(impl_block((ROOT / public_path).read_text(), public_name))
config = setters(impl_block((ROOT / config_path).read_text(), config_name))
missing = sorted(config - public - NOT_OPTIONS)
if missing:
failures.append(
f"{public_name} ({public_path}) is missing {len(missing)} option(s) "
f"that {config_name} exposes:\n"
+ "".join(f" - {name}\n" for name in missing)
)
if failures:
print("API surface parity check FAILED\n", file=sys.stderr)
for failure in failures:
print(failure, file=sys.stderr)
print(
"An option on the config builder that is absent from the public builder\n"
"is implemented, documentable, and uncallable. Either mirror it onto the\n"
"public builder, or add it to NOT_OPTIONS in this script with a reason.",
file=sys.stderr,
)
return 1
total = sum(
len(setters(impl_block((ROOT / cfg_path).read_text(), cfg_name)))
for (_, _), (cfg_name, cfg_path) in PAIRS
)
print(f"✓ API surface parity: {len(PAIRS)} builder pairs, {total} options all reachable")
return 0
if __name__ == "__main__":
sys.exit(main())