First version
This commit is contained in:
1 parent
80c201e1f6
commit
23e3acc2be
1 file changed
+157
+157
@@ -0,0 +1,157 @@
|
|||||||
|
#!/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())
|
||||||
Reference in new issue
Block a user