Files
openstack-api-simulator/app/openstack/request_examples.py
T
inecs 147f04c9a2 Fix Nova/Cinder stateful CRUD so the full surface and Pulumi matrix pass clean.
Persist aggregates and server security groups in domain tables, add Cinder quota
show from DB-backed defaults, align unit tests with OpenStack series, auto-pick
Keystone :5000/:15000 for smoke, and document test locations plus 6514/6514 results.
2026-07-18 09:38:45 +03:00

261 lines
9.1 KiB
Python

"""Build UI body_fields / body_example from JSON Schema request bodies."""
from __future__ import annotations
from typing import Any
def schema_example(schema: dict[str, Any] | None, *, name: str | None = None) -> Any:
"""Build a representative JSON value from a JSON Schema fragment."""
if not schema:
return None
if "example" in schema:
return schema["example"]
if "default" in schema:
return schema["default"]
enum_values = schema.get("enum") or []
if enum_values:
return enum_values[0]
if "const" in schema:
return schema["const"]
schema_type = schema.get("type")
if isinstance(schema_type, list):
schema_type = next((t for t in schema_type if t != "null"), schema_type[0])
if schema_type == "object" or ("properties" in schema and schema_type is None):
props = schema.get("properties") or {}
required = set(schema.get("required") or [])
out: dict[str, Any] = {}
for key, child in props.items():
if not isinstance(child, dict):
continue
# Include required always; also optional fields that declare example/default.
include = key in required or "example" in child or "default" in child
if not include:
# Still include optional leaves for api-ref-style full previews.
include = True
if include:
out[key] = schema_example(child, name=key)
return out
if schema_type == "array":
items = schema.get("items")
if isinstance(items, dict):
return [schema_example(items, name=name)]
return []
if schema_type == "boolean":
return False
if schema_type == "integer":
minimum = schema.get("minimum")
return int(minimum) if minimum is not None else 1
if schema_type == "number":
minimum = schema.get("minimum")
return float(minimum) if minimum is not None else 1.0
if schema_type == "null":
return None
# string / unknown
fmt = schema.get("format")
if fmt == "uri" or fmt == "url":
return "http://example.com"
if fmt == "email":
return "user@example.com"
if fmt == "uuid" or (name and (name.endswith("_id") or name == "id")):
return "00000000-0000-0000-0000-000000000001"
if name in {"name", "stack_name", "display_name"}:
return "example"
if name in {"cidr", "remote_ip_prefix"}:
return "10.0.0.0/24"
if name in {"password"}:
return "secret"
return "example"
def flatten_schema_fields(
schema: dict[str, Any] | None,
*,
prefix: str = "",
max_depth: int = 6,
) -> list[dict[str, Any]]:
"""Flatten a JSON Schema into dotted PARAM field descriptors for the console."""
if not schema or max_depth < 0:
return []
schema_type = schema.get("type")
if isinstance(schema_type, list):
schema_type = next((t for t in schema_type if t != "null"), schema_type[0])
props = schema.get("properties")
if (schema_type == "object" or props) and isinstance(props, dict):
required = set(schema.get("required") or [])
fields: list[dict[str, Any]] = []
for key, child in props.items():
if not isinstance(child, dict):
continue
path = f"{prefix}.{key}" if prefix else key
child_type = child.get("type")
if isinstance(child_type, list):
child_type = next((t for t in child_type if t != "null"), child_type[0])
nested_props = child.get("properties")
if (child_type == "object" or nested_props) and isinstance(nested_props, dict):
fields.extend(flatten_schema_fields(child, prefix=path, max_depth=max_depth - 1))
elif child_type == "array":
items = child.get("items")
if isinstance(items, dict) and (
items.get("type") == "object" or isinstance(items.get("properties"), dict)
):
# Expand one sample element so nested array object fields appear.
fields.extend(
flatten_schema_fields(items, prefix=f"{path}.0", max_depth=max_depth - 1)
)
else:
fields.append(
{
"name": path,
"type": "array",
"description": child.get("description") or f"Array {path}",
"optional": key not in required,
"enum": list(child.get("enum") or []),
"example": schema_example(child, name=key),
}
)
else:
fields.append(
{
"name": path,
"type": str(child_type or "string"),
"description": child.get("description"),
"optional": key not in required,
"enum": list(child.get("enum") or []),
"example": schema_example(child, name=key),
}
)
return fields
if prefix:
return [
{
"name": prefix,
"type": str(schema_type or "string"),
"description": schema.get("description"),
"optional": False,
"enum": list(schema.get("enum") or []),
"example": schema_example(schema, name=prefix),
}
]
return []
def body_fields_from_example(body_example: dict[str, Any] | None) -> list[dict[str, Any]]:
"""PARAM inputs derived from body_example, including nested scalar paths.
Mirrors oVirt console behaviour: walk the Engine/OpenStack-shaped example and
emit one PARAM row per leaf (dotted paths, numeric segments for arrays).
"""
if not isinstance(body_example, dict) or not body_example:
return []
fields: list[dict[str, Any]] = []
def _leaf_type(value: Any) -> str:
if isinstance(value, bool):
return "boolean"
if isinstance(value, int) and not isinstance(value, bool):
return "integer"
if isinstance(value, float):
return "number"
if value is None:
return "null"
return "string"
def _walk(prefix: str, value: Any) -> None:
if isinstance(value, dict):
for key, child in value.items():
path = f"{prefix}.{key}" if prefix else str(key)
_walk(path, child)
return
if isinstance(value, list):
if not value:
fields.append(
{
"name": prefix,
"type": "array",
"description": prefix,
"optional": True,
"enum": [],
"example": [],
}
)
return
for index, child in enumerate(value):
path = f"{prefix}.{index}" if prefix else str(index)
_walk(path, child)
return
fields.append(
{
"name": prefix,
"type": _leaf_type(value),
"description": prefix,
"optional": True,
"enum": [],
"example": value,
}
)
_walk("", body_example)
return fields
def unflatten_body(values: dict[str, Any]) -> dict[str, Any]:
"""Turn dotted keys into a nested dict (supports numeric array segments)."""
root: dict[str, Any] = {}
def _set(path: str, value: Any) -> None:
parts = path.split(".")
cur: Any = root
for i, part in enumerate(parts[:-1]):
nxt = parts[i + 1]
want_array = nxt.isdigit()
if part.isdigit():
idx = int(part)
if not isinstance(cur, list):
return
while len(cur) <= idx:
cur.append([] if want_array else {})
if (
cur[idx] is None
or (want_array and not isinstance(cur[idx], list))
or (not want_array and not isinstance(cur[idx], dict))
):
cur[idx] = [] if want_array else {}
cur = cur[idx]
continue
if want_array:
if not isinstance(cur.get(part), list):
cur[part] = []
elif not isinstance(cur.get(part), dict):
cur[part] = {}
cur = cur[part]
leaf = parts[-1]
if leaf.isdigit():
idx = int(leaf)
if not isinstance(cur, list):
return
while len(cur) <= idx:
cur.append(None)
cur[idx] = value
return
if isinstance(cur, dict):
cur[leaf] = value
for dotted, value in sorted(values.items(), key=lambda item: item[0].count(".")):
if value is None or value == "":
continue
_set(dotted, value)
return root