first init
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when a public service route is missing from docs or OpenAPI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parents[1]
|
||||
REPO = DOCS.parent
|
||||
PUBLIC = DOCS / "public"
|
||||
|
||||
|
||||
def messenger_routes() -> set[tuple[str, str]]:
|
||||
source = (REPO / "micro-chat/micromsg/src/app_routes.rs").read_text(encoding="utf-8")
|
||||
routes = set(re.findall(r'\("([A-Z]+)", "([^"]+)"\)\s*=>', source))
|
||||
routes = {(method, path) for method, path in routes if not path.startswith("/internal/")}
|
||||
if 'path.starts_with("/file/")' in source:
|
||||
routes.add(("GET", "/file/{id}"))
|
||||
return routes
|
||||
|
||||
|
||||
def gateway_routes() -> set[tuple[str, str]]:
|
||||
source = (REPO / "crypto-gateway/crates/gateway-api/src/main.rs").read_text(encoding="utf-8")
|
||||
routes: set[tuple[str, str]] = set()
|
||||
cursor = 0
|
||||
while (start := source.find(".route(", cursor)) != -1:
|
||||
depth = 0
|
||||
end = start + len(".route(")
|
||||
while end < len(source):
|
||||
if source[end] == "(":
|
||||
depth += 1
|
||||
elif source[end] == ")":
|
||||
if depth == 0:
|
||||
break
|
||||
depth -= 1
|
||||
end += 1
|
||||
block = source[start:end + 1]
|
||||
match = re.search(r'\.route\(\s*"([^"]+)"\s*,(.*)\)$', block, re.S)
|
||||
cursor = end + 1
|
||||
if not match:
|
||||
continue
|
||||
path, handlers = match.groups()
|
||||
if path.startswith("/v1/") or path.startswith("/admin/v1/") or path == "/health":
|
||||
for method in re.findall(r'\b(get|post|delete)\s*\(', handlers):
|
||||
routes.add((method.upper(), path))
|
||||
web = (REPO / "crypto-gateway/crates/gateway-api/src/web.rs").read_text(encoding="utf-8")
|
||||
for path in ("/pay/{token}", "/pay/{token}/status"):
|
||||
if f'.route("{path}", get(' not in web:
|
||||
raise AssertionError(f"hosted checkout route disappeared: {path}")
|
||||
routes.add(("GET", path))
|
||||
return routes
|
||||
|
||||
|
||||
def donations_routes() -> set[tuple[str, str]]:
|
||||
source = (REPO / "donations/src/main.rs").read_text(encoding="utf-8")
|
||||
routes: set[tuple[str, str]] = set()
|
||||
cursor = 0
|
||||
while (start := source.find(".route(", cursor)) != -1:
|
||||
depth = 0
|
||||
end = start + len(".route(")
|
||||
while end < len(source):
|
||||
if source[end] == "(":
|
||||
depth += 1
|
||||
elif source[end] == ")":
|
||||
if depth == 0:
|
||||
break
|
||||
depth -= 1
|
||||
end += 1
|
||||
block = source[start:end + 1]
|
||||
match = re.search(r'\.route\(\s*"([^"]+)"\s*,(.*)\)$', block, re.S)
|
||||
cursor = end + 1
|
||||
if not match:
|
||||
continue
|
||||
path, handlers = match.groups()
|
||||
if path == "/health" or path.startswith("/api/"):
|
||||
for method in re.findall(r'\b(get|post|delete|put|patch)\s*\(', handlers):
|
||||
routes.add((method.upper(), path))
|
||||
for method in re.findall(r'\.(delete|put|patch)\s*\(', handlers):
|
||||
routes.add((method.upper(), path))
|
||||
return routes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
manifest = json.loads((PUBLIC / "api-manifest.json").read_text(encoding="utf-8"))
|
||||
documented = {
|
||||
service["id"]: {(item["method"], item["path"]) for item in service["endpoints"]}
|
||||
for service in manifest["services"]
|
||||
}
|
||||
source = {
|
||||
"messenger": messenger_routes(),
|
||||
"gateway": gateway_routes(),
|
||||
"donations": donations_routes(),
|
||||
}
|
||||
failed = False
|
||||
for service_id, actual in source.items():
|
||||
missing = actual - documented[service_id]
|
||||
stale = documented[service_id] - actual
|
||||
if missing:
|
||||
print(f"{service_id}: undocumented routes: {sorted(missing)}", file=sys.stderr)
|
||||
failed = True
|
||||
if stale:
|
||||
print(f"{service_id}: stale routes: {sorted(stale)}", file=sys.stderr)
|
||||
failed = True
|
||||
spec = json.loads((PUBLIC / "openapi" / f"{service_id}.json").read_text(encoding="utf-8"))
|
||||
in_spec = {(method.upper(), path) for path, operations in spec["paths"].items()
|
||||
for method in operations if method.upper() in {"GET", "POST", "DELETE", "PUT", "PATCH"}}
|
||||
if in_spec != documented[service_id]:
|
||||
print(f"{service_id}: OpenAPI mismatch", file=sys.stderr)
|
||||
failed = True
|
||||
counted = sum(len(routes) for routes in documented.values())
|
||||
if counted != manifest["total_endpoints"]:
|
||||
print(f"manifest total is {manifest['total_endpoints']}, counted {counted}", file=sys.stderr)
|
||||
failed = True
|
||||
if failed:
|
||||
return 1
|
||||
print(f"verified {counted} public endpoints across {len(source)} services")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user