This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when the documented Pulse API and registered HTTP routes drift apart."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST = ROOT / "specs" / "api-routes.json"
|
||||
CONTRACT = ROOT / "docs" / "architecture" / "API_CONTRACT.md"
|
||||
ROUTERS = (ROOT / "cmd" / "api" / "main.go", ROOT / "internal" / "service" / "health.go")
|
||||
|
||||
TABLE_ROUTE = re.compile(r"^\| `(?P<method>GET|POST|PUT|PATCH|DELETE) (?P<path>/[^`]+)` \|", re.MULTILINE)
|
||||
REGISTERED = re.compile(r"\.Handle(?:Func)?\(\s*\"(?P<path>/[^\"]+)\"")
|
||||
|
||||
|
||||
def fail(messages: list[str]) -> int:
|
||||
for message in messages:
|
||||
print(f"API CONTRACT: FAIL: {message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
document = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
routes = document.get("routes", [])
|
||||
errors: list[str] = []
|
||||
|
||||
keys: list[tuple[str, str]] = []
|
||||
covered_registrations: set[str] = set()
|
||||
for index, route in enumerate(routes):
|
||||
missing = {"method", "path", "registration", "access", "implementation"} - route.keys()
|
||||
if missing:
|
||||
errors.append(f"route {index} misses fields: {', '.join(sorted(missing))}")
|
||||
continue
|
||||
key = (route["method"], route["path"])
|
||||
if key in keys:
|
||||
errors.append(f"duplicate manifest route: {key[0]} {key[1]}")
|
||||
keys.append(key)
|
||||
covered_registrations.add(route["registration"])
|
||||
implementation = ROOT / route["implementation"]
|
||||
if not implementation.is_file():
|
||||
errors.append(f"implementation does not exist for {key[0]} {key[1]}: {implementation.relative_to(ROOT)}")
|
||||
|
||||
markdown = CONTRACT.read_text(encoding="utf-8")
|
||||
documented = [(match.group("method"), match.group("path")) for match in TABLE_ROUTE.finditer(markdown)]
|
||||
manifest_set = set(keys)
|
||||
documented_set = set(documented)
|
||||
for method, path in sorted(manifest_set - documented_set):
|
||||
errors.append(f"manifest route is not documented: {method} {path}")
|
||||
for method, path in sorted(documented_set - manifest_set):
|
||||
errors.append(f"documented route is not in manifest: {method} {path}")
|
||||
if len(documented) != len(documented_set):
|
||||
errors.append("the route table contains duplicate method/path rows")
|
||||
|
||||
registered: set[str] = set()
|
||||
for source in ROUTERS:
|
||||
registered.update(match.group("path") for match in REGISTERED.finditer(source.read_text(encoding="utf-8")))
|
||||
for path in sorted(registered - covered_registrations):
|
||||
errors.append(f"registered router path is not represented in the manifest: {path}")
|
||||
for path in sorted(covered_registrations - registered):
|
||||
errors.append(f"manifest registration does not exist in the router: {path}")
|
||||
|
||||
if errors:
|
||||
return fail(errors)
|
||||
print(
|
||||
"API CONTRACT: PASS "
|
||||
f"({len(manifest_set)} method/path contracts, {len(registered)} router registrations, no drift)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user