263 lines
9.2 KiB
Python
263 lines
9.2 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
|