Add swagger-drogon generator

This commit is contained in:
gabdevele
2026-06-03 01:52:43 +02:00 Unverified
parent 1043768da7
commit 21883a4a8f
19 changed files with 1672 additions and 203 deletions
@@ -0,0 +1,163 @@
from __future__ import annotations
import copy
import re
from typing import Any
from .models import InferredOperation, Route
from .parser import parse_tags
METHODS_WITH_JSON_BODY = {"post", "put", "patch"}
def build_path(raw_path: str, names: list[str] | None = None) -> tuple[str, list[dict[str, Any]]]:
parameters: list[dict[str, Any]] = []
names = names or []
def replace(match: re.Match[str]) -> str:
index = int(match.group(1)) - 1
name = names[index] if index < len(names) else ("id" if index == 0 else f"param{index + 1}")
parameters.append(
{
"name": name,
"in": "path",
"required": True,
"schema": {"type": "integer", "format": "int64"},
}
)
return "{" + name + "}"
return re.sub(r"\{(\d+)\}", replace, raw_path), parameters
def build_operation(
route: Route,
inferred: InferredOperation,
operation_contract: dict[str, Any],
) -> dict[str, Any]:
metadata = route.metadata
response_schema = _schema_from_contract(operation_contract, "response")
if response_schema is None:
response_schema = inferred.response_schema
op: dict[str, Any] = {
"tags": parse_tags(metadata["tags"]),
"operationId": route.operation_id,
"summary": metadata["summary"],
"description": metadata["description"],
"parameters": [*inferred.path_parameters, *inferred.query_parameters],
"responses": _responses(metadata["response"], response_schema, operation_contract),
}
request_body = _request_body(route, inferred, operation_contract)
if request_body is not None:
op["requestBody"] = request_body
if metadata["auth"] == "bearer":
op["security"] = [{"bearerAuth": []}]
elif metadata["auth"] != "public":
raise ValueError(f"{route.header}: unsupported auth kind: {metadata['auth']}")
return op
def merge_components(contract: dict[str, Any]) -> dict[str, Any]:
components = copy.deepcopy(contract.get("components", {}))
components.setdefault("securitySchemes", {})
components["securitySchemes"].setdefault(
"bearerAuth", {"type": "http", "scheme": "bearer"}
)
return components
def _request_body(
route: Route,
inferred: InferredOperation,
operation_contract: dict[str, Any],
) -> dict[str, Any] | None:
body_kind = route.metadata["body"]
if body_kind == "none":
return None
explicit = operation_contract.get("requestBody")
if explicit is not None:
return copy.deepcopy(explicit)
schema = _schema_from_contract(operation_contract, "request")
if schema is None:
schema = inferred.request_schema
if body_kind == "multipart":
if schema is None:
schema = {
"type": "object",
"required": ["files"],
"properties": {
"files": {
"type": "array",
"items": {"type": "string", "format": "binary"},
}
},
}
return {
"required": True,
"content": {"multipart/form-data": {"schema": schema}},
}
if body_kind == "json" and route.http_method in METHODS_WITH_JSON_BODY:
return {
"required": True,
"content": {"application/json": {"schema": schema or {"type": "object"}}},
}
raise ValueError(
f"{route.header}: unsupported body kind {body_kind} for {route.http_method.upper()}"
)
def _responses(
description: str,
response_schema: dict[str, Any] | None,
operation_contract: dict[str, Any],
) -> dict[str, Any]:
if "responses" in operation_contract:
return copy.deepcopy(operation_contract["responses"])
content_type = operation_contract.get("responseContentType", "application/json")
if content_type == "text/html":
success = {
"description": description,
"content": {"text/html": {"schema": {"type": "string"}}},
}
else:
success = {
"description": description,
"content": {
content_type: {
"schema": response_schema or {"type": "object"},
}
},
}
responses: dict[str, Any] = {"200": success}
if operation_contract.get("created", False):
responses = {"201": success}
for status, response in operation_contract.get("errors", {}).items():
responses[str(status)] = copy.deepcopy(response)
return responses
def _schema_from_contract(
operation_contract: dict[str, Any],
key: str,
) -> dict[str, Any] | None:
value = operation_contract.get(key)
if value is None:
return None
if isinstance(value, str):
return {"$ref": value}
if isinstance(value, dict):
return copy.deepcopy(value)
raise ValueError(f"operation contract field {key} must be string or object")