Add swagger-drogon generator
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user