148 lines
4.3 KiB
Python
148 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a compact OpenAPI spec from Drogon HttpController route macros."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
ROUTE_RE = re.compile(
|
|
r"ADD_METHOD_TO\(\s*([A-Za-z_]\w*)::([A-Za-z_]\w*)\s*,\s*"
|
|
r'"([^"]+)"\s*,\s*drogon::([A-Za-z]+)\s*\)',
|
|
re.MULTILINE,
|
|
)
|
|
|
|
|
|
METHODS_WITH_JSON_BODY = {"post", "put", "patch"}
|
|
PUBLIC_OPERATIONS = {
|
|
"health",
|
|
"ready",
|
|
"openApi",
|
|
"docs",
|
|
"registerCitizen",
|
|
"requestOtp",
|
|
"verifyOtp",
|
|
"loginWithPassword",
|
|
}
|
|
|
|
|
|
def controller_tag(controller: str) -> str:
|
|
return controller.removesuffix("Controller")
|
|
|
|
|
|
def openapi_path(path: str) -> tuple[str, list[dict]]:
|
|
parameters: list[dict] = []
|
|
|
|
def replace(match: re.Match[str]) -> str:
|
|
name = "id" if match.group(1) == "1" else f"param{match.group(1)}"
|
|
parameters.append(
|
|
{
|
|
"name": name,
|
|
"in": "path",
|
|
"required": True,
|
|
"schema": {"type": "integer", "format": "int64"},
|
|
}
|
|
)
|
|
return "{" + name + "}"
|
|
|
|
return re.sub(r"\{(\d+)\}", replace, path), parameters
|
|
|
|
|
|
def request_body(path: str, method: str) -> dict | None:
|
|
if method != "post" and method != "patch":
|
|
return None
|
|
if path.endswith("/media") or path.endswith("/photos"):
|
|
return {
|
|
"required": True,
|
|
"content": {
|
|
"multipart/form-data": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"files": {
|
|
"type": "array",
|
|
"items": {"type": "string", "format": "binary"},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
if method in METHODS_WITH_JSON_BODY:
|
|
return {
|
|
"required": True,
|
|
"content": {
|
|
"application/json": {"schema": {"type": "object"}}
|
|
},
|
|
}
|
|
return None
|
|
|
|
|
|
def operation(controller: str, handler: str, raw_path: str, method: str) -> dict:
|
|
path, params = openapi_path(raw_path)
|
|
op = {
|
|
"tags": [controller_tag(controller)],
|
|
"operationId": f"{controller}.{handler}",
|
|
"parameters": params,
|
|
"responses": {
|
|
"200": {
|
|
"description": "JSON response",
|
|
"content": {"application/json": {"schema": {"type": "object"}}},
|
|
}
|
|
},
|
|
}
|
|
body = request_body(path, method)
|
|
if body:
|
|
op["requestBody"] = body
|
|
if handler not in PUBLIC_OPERATIONS:
|
|
op["security"] = [{"bearerAuth": []}]
|
|
if method == "post":
|
|
op["responses"]["201"] = op["responses"]["200"]
|
|
return path, op
|
|
|
|
|
|
def generate(controllers_dir: Path, title: str, version: str) -> dict:
|
|
spec = {
|
|
"openapi": "3.0.3",
|
|
"info": {
|
|
"title": title,
|
|
"version": version,
|
|
"description": "Generated from Drogon controller route macros.",
|
|
},
|
|
"servers": [{"url": "http://localhost:5555"}],
|
|
"components": {
|
|
"securitySchemes": {
|
|
"bearerAuth": {"type": "http", "scheme": "bearer"}
|
|
}
|
|
},
|
|
"paths": {},
|
|
}
|
|
for header in sorted(controllers_dir.glob("*.h")):
|
|
text = header.read_text(encoding="utf-8")
|
|
for controller, handler, raw_path, drogon_method in ROUTE_RE.findall(text):
|
|
method = drogon_method.lower()
|
|
path, op = operation(controller, handler, raw_path, method)
|
|
spec["paths"].setdefault(path, {})[method] = op
|
|
return spec
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--controllers", type=Path, required=True)
|
|
parser.add_argument("--out", type=Path, required=True)
|
|
parser.add_argument("--title", default="Visione API")
|
|
parser.add_argument("--version", default="0.1.0")
|
|
args = parser.parse_args()
|
|
|
|
spec = generate(args.controllers, args.title, args.version)
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out.write_text(json.dumps(spec, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|