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,99 @@
from __future__ import annotations
from typing import Any
def validate_spec(spec: dict[str, Any]) -> None:
_validate_no_photos(spec)
_validate_refs(spec)
_validate_operations(spec)
def _validate_no_photos(spec: dict[str, Any]) -> None:
for path in spec.get("paths", {}):
if "/photos" in path:
raise ValueError(f"generated OpenAPI path still uses /photos: {path}")
def _validate_refs(spec: dict[str, Any]) -> None:
for ref in _walk_refs(spec):
_resolve_ref(spec, ref)
def _validate_operations(spec: dict[str, Any]) -> None:
for path, path_item in spec.get("paths", {}).items():
for method, operation in path_item.items():
label = f"{method.upper()} {path}"
if method.lower() == "get" and "requestBody" in operation:
raise ValueError(f"{label}: GET operation must not have requestBody")
request_body = operation.get("requestBody")
if request_body is not None:
for schema in _content_schemas(request_body):
if _is_generic_object(spec, schema):
raise ValueError(f"{label}: request schema is generic object")
success = _success_response(operation)
if success is None:
raise ValueError(f"{label}: missing 2xx response")
for schema in _content_schemas(success):
if _is_generic_object(spec, schema):
raise ValueError(f"{label}: response schema is generic object")
def _success_response(operation: dict[str, Any]) -> dict[str, Any] | None:
for status in sorted(operation.get("responses", {})):
if str(status).startswith("2"):
return operation["responses"][status]
return None
def _content_schemas(container: dict[str, Any]) -> list[dict[str, Any]]:
schemas: list[dict[str, Any]] = []
for media in container.get("content", {}).values():
schema = media.get("schema")
if isinstance(schema, dict):
schemas.append(schema)
return schemas
def _is_generic_object(spec: dict[str, Any], schema: dict[str, Any]) -> bool:
resolved = _resolve_schema(spec, schema)
if resolved.get("type") != "object":
return False
meaningful = {"properties", "additionalProperties", "oneOf", "anyOf", "allOf"}
return not any(key in resolved for key in meaningful)
def _resolve_schema(spec: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
if "$ref" not in schema:
return schema
resolved = _resolve_ref(spec, schema["$ref"])
if not isinstance(resolved, dict):
raise ValueError(f"{schema['$ref']}: does not resolve to an object")
return resolved
def _resolve_ref(spec: dict[str, Any], ref: str) -> Any:
if not ref.startswith("#/"):
raise ValueError(f"external refs are not supported: {ref}")
current: Any = spec
for part in ref[2:].split("/"):
if not isinstance(current, dict) or part not in current:
raise ValueError(f"unresolved OpenAPI ref: {ref}")
current = current[part]
return current
def _walk_refs(value: Any) -> list[str]:
refs: list[str] = []
if isinstance(value, dict):
ref = value.get("$ref")
if isinstance(ref, str):
refs.append(ref)
for child in value.values():
refs.extend(_walk_refs(child))
elif isinstance(value, list):
for child in value:
refs.extend(_walk_refs(child))
return refs