78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
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
|