128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""Load and resolve OpenStack request-body JSON Schemas.
|
|
|
|
Schemas live in ``contracts/openstack/request_bodies/<service>.json`` and are
|
|
merged onto ``OperationSpec`` at pack load time (shared across series).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import replace
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.openstack.opspec import OperationSpec
|
|
|
|
_BODIES_ROOT = Path(__file__).resolve().parents[2] / "contracts" / "openstack" / "request_bodies"
|
|
|
|
|
|
def request_bodies_root() -> Path:
|
|
return _BODIES_ROOT
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _load_all() -> dict[str, dict[str, Any]]:
|
|
root = request_bodies_root()
|
|
out: dict[str, dict[str, Any]] = {}
|
|
if not root.is_dir():
|
|
return out
|
|
for path in sorted(root.glob("*.json")):
|
|
data = json.loads(path.read_text())
|
|
service = str(data.get("service") or path.stem)
|
|
ops = data.get("operations") or {}
|
|
by_path = data.get("by_path") or {}
|
|
out[service] = {"operations": dict(ops), "by_path": dict(by_path)}
|
|
return out
|
|
|
|
|
|
def clear_request_body_cache() -> None:
|
|
_load_all.cache_clear()
|
|
|
|
|
|
def _path_key(method: str, path: str) -> str:
|
|
return f"{method.upper()} {path}"
|
|
|
|
|
|
def lookup_request_schema(service: str, op: OperationSpec) -> dict[str, Any] | None:
|
|
"""Return the JSON Schema for an operation, or None if missing."""
|
|
|
|
store = _load_all().get(service) or {}
|
|
ops = store.get("operations") or {}
|
|
schema = ops.get(op.operation_id)
|
|
if schema is None:
|
|
schema = (store.get("by_path") or {}).get(_path_key(op.method, op.path))
|
|
if schema is None:
|
|
return None
|
|
return _resolve_schema(schema, microversion=op.microversion_max)
|
|
|
|
|
|
def _resolve_schema(schema: dict[str, Any], *, microversion: str | None) -> dict[str, Any]:
|
|
"""Pick a concrete schema from OpenStack oneOf discriminators when present."""
|
|
|
|
if not isinstance(schema, dict):
|
|
return {}
|
|
if "oneOf" in schema and isinstance(schema["oneOf"], list):
|
|
xos = schema.get("x-openstack") or {}
|
|
discriminator = xos.get("discriminator")
|
|
variants = [item for item in schema["oneOf"] if isinstance(item, dict)]
|
|
if discriminator == "action":
|
|
# Prefer first variant; callers match action via separate ops.
|
|
chosen = variants[0] if variants else schema
|
|
return _resolve_schema(chosen, microversion=microversion)
|
|
if discriminator == "microversion" and microversion:
|
|
best: dict[str, Any] | None = None
|
|
for item in variants:
|
|
meta = item.get("x-openstack") or {}
|
|
min_ver = str(meta.get("min-ver") or "0")
|
|
max_ver = meta.get("max-ver")
|
|
if _mv_le(min_ver, microversion) and (
|
|
max_ver is None or _mv_le(microversion, str(max_ver))
|
|
):
|
|
best = item
|
|
if best is not None:
|
|
return _resolve_schema(best, microversion=microversion)
|
|
if variants:
|
|
return _resolve_schema(variants[-1], microversion=microversion)
|
|
return schema
|
|
|
|
|
|
def _mv_le(left: str, right: str) -> bool:
|
|
def parts(value: str) -> tuple[int, ...]:
|
|
out: list[int] = []
|
|
for piece in value.split("."):
|
|
try:
|
|
out.append(int(piece))
|
|
except ValueError:
|
|
out.append(0)
|
|
return tuple(out)
|
|
|
|
return parts(left) <= parts(right)
|
|
|
|
|
|
def attach_request_schemas(service: str, ops: list[OperationSpec]) -> list[OperationSpec]:
|
|
"""Return new OperationSpec list with ``request_schema`` filled where known."""
|
|
|
|
attached: list[OperationSpec] = []
|
|
for op in ops:
|
|
schema = lookup_request_schema(service, op)
|
|
if schema is None:
|
|
attached.append(op)
|
|
continue
|
|
attached.append(replace(op, request_schema=schema))
|
|
return attached
|
|
|
|
|
|
def missing_write_schemas(packs: dict[str, Any]) -> list[tuple[str, str, str, str]]:
|
|
"""List (service, method, path, operation_id) missing request schemas."""
|
|
|
|
missing: list[tuple[str, str, str, str]] = []
|
|
for name, pack in sorted(packs.items()):
|
|
for op in pack.operations:
|
|
if op.method not in {"POST", "PUT", "PATCH"}:
|
|
continue
|
|
if op.request_schema:
|
|
continue
|
|
missing.append((name, op.method, op.path, op.operation_id))
|
|
return missing
|