Adds docs/testing.md as the authoritative testing map (consolidated from docs/contributing.md §8, now shrunk to a pointer), and a generated docs/endpoint-index.md via scripts/generate-endpoint-index.py (hooked into scripts/update-openapi.sh). Updates docs/README.md's reading order and removes the "still to come" placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.0 KiB
Python
Executable File
96 lines
3.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate docs/endpoint-index.md from the OpenAPI spec.
|
|
|
|
Do not edit docs/endpoint-index.md by hand -- it is generated by this script
|
|
from ErsatzTV/wwwroot/openapi/v1.json. Regenerated automatically as part of
|
|
scripts/update-openapi.sh.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SPEC_PATH = REPO_ROOT / "ErsatzTV" / "wwwroot" / "openapi" / "v1.json"
|
|
OUTPUT_PATH = REPO_ROOT / "docs" / "endpoint-index.md"
|
|
|
|
UNTAGGED = "(untagged)"
|
|
|
|
|
|
def load_operations(spec: dict) -> list[dict]:
|
|
"""Flatten the OpenAPI paths object into a list of per-operation records."""
|
|
operations = []
|
|
for path, path_item in spec.get("paths", {}).items():
|
|
for method, operation in path_item.items():
|
|
if method.lower() not in HTTP_METHODS:
|
|
continue
|
|
operations.append(
|
|
{
|
|
"path": path,
|
|
"method": method.upper(),
|
|
"operationId": operation.get("operationId", ""),
|
|
"summary": operation.get("summary", "") or "",
|
|
"tags": operation.get("tags") or [UNTAGGED],
|
|
}
|
|
)
|
|
return operations
|
|
|
|
|
|
def group_by_tag(operations: list[dict]) -> dict[str, list[dict]]:
|
|
grouped: dict[str, list[dict]] = {}
|
|
for op in operations:
|
|
for tag in op["tags"]:
|
|
grouped.setdefault(tag, []).append(op)
|
|
return grouped
|
|
|
|
|
|
def render(spec: dict) -> str:
|
|
operations = load_operations(spec)
|
|
grouped = group_by_tag(operations)
|
|
|
|
tags = sorted(t for t in grouped if t != UNTAGGED)
|
|
if UNTAGGED in grouped:
|
|
tags.append(UNTAGGED)
|
|
|
|
lines = []
|
|
lines.append("# API endpoint index")
|
|
lines.append("")
|
|
lines.append(
|
|
"*Generated by `scripts/generate-endpoint-index.py` from "
|
|
"`ErsatzTV/wwwroot/openapi/v1.json`. Do not edit by hand -- regenerated by "
|
|
"`scripts/update-openapi.sh`.*"
|
|
)
|
|
lines.append("")
|
|
lines.append(
|
|
f"{len(spec.get('paths', {}))} endpoints, {len(operations)} operations."
|
|
)
|
|
lines.append("")
|
|
|
|
for tag in tags:
|
|
lines.append(f"## {tag}")
|
|
lines.append("")
|
|
lines.append("| Method | Path | Operation | Summary |")
|
|
lines.append("|---|---|---|---|")
|
|
for op in sorted(grouped[tag], key=lambda o: (o["path"], o["method"])):
|
|
lines.append(
|
|
f"| {op['method']} | `{op['path']}` | {op['operationId']} | {op['summary']} |"
|
|
)
|
|
lines.append("")
|
|
|
|
return "\n".join(lines).rstrip("\n") + "\n"
|
|
|
|
|
|
def main() -> None:
|
|
spec = json.loads(SPEC_PATH.read_text())
|
|
OUTPUT_PATH.write_text(render(spec))
|
|
operations = load_operations(spec)
|
|
print(
|
|
f"Wrote {OUTPUT_PATH.relative_to(REPO_ROOT)}: "
|
|
f"{len(spec.get('paths', {}))} endpoints, {len(operations)} operations."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|