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