164 lines
4.9 KiB
Python
164 lines
4.9 KiB
Python
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")
|