#!/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: '") 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())