Files
2026-11-06 16:16:55 +01:00

157 lines
4.9 KiB
Python

#!/usr/bin/env python
"""Generate API.md from the *openapi.json files in sources/."""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
SOURCES = ROOT / "sources"
GATEWAY_URL = "https://notifier.gansejunge.com/internal/"
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
def resolve(schema, spec):
"""Follow $ref pointers like #/components/schemas/Foo."""
while "$ref" in schema:
node = spec
for part in schema["$ref"].lstrip("#/").split("/"):
node = node[part]
schema = node
return schema
def example(schema, spec, depth=0):
"""Build an example value from a JSON schema."""
if depth > 6:
return None
s = resolve(schema, spec)
if "example" in s:
return s["example"]
if "default" in s:
return s["default"]
if "enum" in s:
return s["enum"][0]
for key in ("anyOf", "oneOf"):
if key in s:
options = [o for o in s[key] if o.get("type") != "null"]
return example(options[0], spec, depth + 1) if options else None
if "allOf" in s:
return example(s["allOf"][0], spec, depth + 1)
t = s.get("type")
if t == "object" or "properties" in s:
return {k: example(v, spec, depth + 1) for k, v in s.get("properties", {}).items()}
if t == "array":
return [example(s.get("items", {}), spec, depth + 1)]
return {"string": "string", "integer": 0, "number": 0.0, "boolean": True}.get(t)
def type_label(schema, spec):
s = resolve(schema, spec)
for key in ("anyOf", "oneOf"):
if key in s:
return " | ".join(type_label(o, spec) for o in s[key])
if s.get("type") == "array":
return f"array[{type_label(s.get('items', {}), spec)}]"
return s.get("type", "object" if "properties" in s else "any")
def build_url(base, path, params):
url = base + path
for p in params:
if p["in"] == "path":
url = url.replace("{%s}" % p["name"], f"<{p['name']}>")
required_query = [p["name"] for p in params if p["in"] == "query" and p.get("required")]
if required_query:
url += "?" + "&".join(f"{q}=<{q}>" for q in required_query)
return url
def render_operation(base, path, method, op, path_params, spec):
params = path_params + op.get("parameters", [])
out = [f"### `{method.upper()} {path}`\n"]
if op.get("summary"):
out.append(f"**{op['summary']}**\n")
if op.get("description"):
out.append(op["description"] + "\n")
if params:
out.append("| Name | In | Type | Required |")
out.append("|---|---|---|---|")
for p in params:
p = resolve(p, spec)
out.append(
f"| `{p['name']}` | {p['in']} | "
f"{type_label(p.get('schema', {}), spec)} | {'yes' if p.get('required') else 'no'} |"
)
out.append("")
curl = [f"curl -X {method.upper()} '{build_url(base, path, params)}'"]
curl.append(" -H 'X-API-Key: <api_key>'")
body = op.get("requestBody", {}).get("content", {})
if "application/json" in body:
ex = example(body["application/json"].get("schema", {}), spec)
out.append("**Request body:**\n")
out.append("```json\n" + json.dumps(ex, indent=2) + "\n```\n")
curl.append(" -H 'Content-Type: application/json'")
curl.append(" -d '" + json.dumps(ex) + "'")
elif body:
out.append(f"**Request body:** `{', '.join(body)}`\n")
out.append("**Example:**\n")
out.append("```bash\n" + " \\\n".join(curl) + "\n```\n")
responses = op.get("responses", {})
if responses:
out.append("**Responses:**\n")
for code, resp in responses.items():
resp = resolve(resp, spec)
line = f"- `{code}`: {resp.get('description', '')}"
json_content = resp.get("content", {}).get("application/json")
if json_content and "schema" in json_content:
ex = example(json_content["schema"], spec)
if ex is not None:
line += "\n ```json\n " + json.dumps(ex, indent=2).replace("\n", "\n ") + "\n ```"
out.append(line)
out.append("")
return "\n".join(out)
def render_service(spec, source_name):
title = spec.get("info", {}).get("title")
if not title:
raise ValueError(f"{source_name} has no info.title")
# The title is the path segment on the gateway
base = GATEWAY_URL.rstrip("/") + "/" + title.strip("/")
out = [f"# {title}\n", f"Base URL: `{base}`\n"]
for path, item in sorted(spec.get("paths", {}).items()):
path_params = item.get("parameters", [])
for method, op in item.items():
if method in HTTP_METHODS:
out.append(render_operation(base, path, method, op, path_params, spec))
return title, "\n".join(out)
def main():
files = sorted(SOURCES.rglob("*openapi.json"))
if not files:
print(f"No *openapi.json files found in {SOURCES}")
return 1
for f in files:
try:
title, markdown = render_service(json.loads(f.read_text(encoding="utf-8")), f.name)
safe_title = title.strip("/").replace("/", "-")
out_file = ROOT / f"API-{safe_title}.md"
out_file.write_text(markdown, encoding="utf-8")
print(f"OK {f.relative_to(ROOT)} -> {out_file.name}")
except Exception as e:
print(f"FAIL {f.relative_to(ROOT)}: {e}")
return 0
if __name__ == "__main__":
raise SystemExit(main())