from __future__ import annotations import re from pathlib import Path from .models import Route 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 | re.DOTALL, ) REQUIRED_METADATA = {"tags", "summary", "description", "auth", "body", "response"} def parse_metadata(header: Path, text: str, route_start: int) -> dict[str, str]: prefix = text[:route_start].rstrip() if not prefix.endswith("*/"): raise ValueError(f"{header}: route is missing an openapi metadata block") comment_start = prefix.rfind("/*") if comment_start == -1: raise ValueError(f"{header}: route is missing an openapi metadata block") block = prefix[comment_start + 2:-2] metadata: dict[str, str] = {} in_openapi_block = False for raw_line in block.splitlines(): line = raw_line.strip() if line.startswith("*"): line = line[1:].strip() if not line: continue if line == "openapi:": in_openapi_block = True continue if not in_openapi_block: continue match = re.match(r"([A-Za-z_]+):\s*(.+)", line) if match: metadata[match.group(1)] = match.group(2).strip() missing = sorted(REQUIRED_METADATA - metadata.keys()) if missing: names = ", ".join(missing) raise ValueError(f"{header}: openapi metadata block is missing: {names}") return metadata def parse_routes(controllers_dir: Path) -> list[Route]: routes: list[Route] = [] for header in sorted(controllers_dir.glob("*.h")): text = header.read_text(encoding="utf-8") for match in ROUTE_RE.finditer(text): controller, handler, raw_path, drogon_method = match.groups() routes.append( Route( controller=controller, handler=handler, raw_path=raw_path, http_method=drogon_method.lower(), metadata=parse_metadata(header, text, match.start()), header=header, ) ) return routes def parse_tags(value: str) -> list[str]: return [tag.strip() for tag in value.split(",") if tag.strip()]