100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
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
|