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")