"""OpenAPI spec validator. Validates an OpenAPI spec dict for required fields and basic structural correctness. Returns a list of human-readable error strings. """ from __future__ import annotations _SUPPORTED_VERSIONS = ("3.0.", "3.1.") _REQUIRED_FIELDS = ("openapi", "info", "paths") def validate_spec(spec_dict: object) -> list[str]: """Validate an OpenAPI spec dict. Returns a list of error strings. An empty list means the spec is valid. Does not raise; all errors are captured and returned. """ if not isinstance(spec_dict, dict): return [f"Spec must be a dict, got {type(spec_dict).__name__}"] errors: list[str] = [] # Check required top-level fields for field in _REQUIRED_FIELDS: if field not in spec_dict: errors.append(f"Missing required field: '{field}'") # Validate openapi version if present if "openapi" in spec_dict: version = spec_dict["openapi"] if not isinstance(version, str): errors.append(f"'openapi' must be a string, got {type(version).__name__}") elif not any(version.startswith(prefix) for prefix in _SUPPORTED_VERSIONS): errors.append( f"Unsupported OpenAPI version '{version}'. " f"Supported versions start with: {', '.join(_SUPPORTED_VERSIONS)}" ) # Validate info object if present if "info" in spec_dict and isinstance(spec_dict["info"], dict): info = spec_dict["info"] for sub_field in ("title", "version"): if sub_field not in info: errors.append(f"Missing required field in 'info': '{sub_field}'") # Validate paths object if present if "paths" in spec_dict and not isinstance(spec_dict["paths"], dict): errors.append(f"'paths' must be a dict, got {type(spec_dict['paths']).__name__}") return errors