from __future__ import annotations import re from pathlib import Path from typing import Any from .infer import infer_operation, load_sources from .models import Route from .openapi import build_operation, build_path, merge_components from .parser import parse_routes from .validate import validate_spec def generate( controllers_dir: Path, source_dirs: list[Path], contract: dict[str, Any], title: str, version: str, server: str, ) -> dict[str, Any]: sources = load_sources(source_dirs) spec: dict[str, Any] = { "openapi": "3.0.3", "info": { "title": title, "version": version, "description": contract.get( "description", "Generated from Drogon route macros, C++ metadata and schemas.", ), }, "servers": [{"url": server}], "components": merge_components(contract), "paths": {}, } operations_contract = contract.get("operations", {}) if not isinstance(operations_contract, dict): raise ValueError("contract operations must be an object") for route in parse_routes(controllers_dir): inferred = infer_operation(route, sources) path, path_params = build_path(route.raw_path, _handler_path_param_names(route)) inferred.path_parameters = path_params operation_contract = operations_contract.get(route.operation_id, {}) if not isinstance(operation_contract, dict): raise ValueError(f"{route.operation_id}: operation contract must be an object") operation = build_operation(route, inferred, operation_contract) spec["paths"].setdefault(path, {})[route.http_method] = operation validate_spec(spec) return spec def _handler_path_param_names(route: Route) -> list[str]: text = route.header.read_text(encoding="utf-8") match = re.search( rf"void\s+{route.handler}\s*\((.*?)\)\s*;", text, re.MULTILINE | re.DOTALL, ) if not match: return [] params = match.group(1) names: list[str] = [] for type_name, name in re.findall(r"\b(long long|int|size_t)\s+([A-Za-z_]\w*)", params): del type_name if name not in {"req"}: names.append(_openapi_param_name(name)) return names def _openapi_param_name(name: str) -> str: if name.endswith("Id") and len(name) > 2: return "id" if name == "reportId" else name[:-2] + "_id" return name