"""Full JSON Schema export for the HydroModPy configuration.
Produces three companion JSON files ready to be consumed by an external
frontend that renders a formular without having to know anything about
Python:
* ``config.json`` - the full JSON Schema of ``HydroModPyConfig``.
* ``config_meta.json`` - high-level metadata (ordered root sections,
UI groups, per-section titles).
* ``field_validators.json`` - flat mapping ``field_path -> validator_type``
so the frontend can pick a widget + a local
validator without re-parsing the schema.
The exporter reuses :mod:`hydromodpy.config.schema_export` for the raw
Pydantic schema and adds the two companion documents on top.
Usage::
from hydromodpy.schema import export_full_schema
export_full_schema("./schema/")
CLI::
hmp dev schema export --output ./schema/
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from hydromodpy.config.schema_export import (
_cached_full_schema,
_ensure_root_sections,
export_schema,
)
from hydromodpy.core.config_kit.registry import root_scalar_fields
SCHEMA_FILE = "config.json"
META_FILE = "config_meta.json"
VALIDATORS_FILE = "field_validators.json"
def _infer_validator_type(field_schema: dict[str, Any]) -> str:
"""Map a JSON Schema field entry to a high-level validator type."""
if "enum" in field_schema:
return "enum"
json_type = field_schema.get("type")
if isinstance(json_type, list):
json_type = next((t for t in json_type if t != "null"), json_type[0])
if json_type in ("integer", "number"):
return "number"
if json_type == "boolean":
return "boolean"
if json_type == "array":
return "array"
if json_type == "object":
return "object"
if json_type == "string":
fmt = field_schema.get("format")
if fmt in ("date", "date-time", "duration"):
return fmt
if fmt == "path":
return "path"
return "string"
if "$ref" in field_schema:
return "nested"
return "any"
def _walk_fields(
section_name: str,
model_cls: type,
out: dict[str, str],
) -> None:
"""Populate *out* with ``field_path -> validator_type`` entries."""
try:
schema = _cached_full_schema(model_cls)
except Exception:
return
props = schema.get("properties", {})
for field_name, field_schema in props.items():
path = f"{section_name}.{field_name}"
out[path] = _infer_validator_type(field_schema)
[docs]
def build_field_validators() -> dict[str, str]:
"""Flatten every root-level field into a ``path -> validator_type`` map."""
sections = _ensure_root_sections()
root_fields = root_scalar_fields()
root_schema = export_schema().get("properties", {})
flat: dict[str, str] = {}
for field_name in root_fields:
field_schema = root_schema.get(field_name, {})
flat[field_name] = _infer_validator_type(field_schema)
for section_name, cls in sections.items():
_walk_fields(section_name, cls, flat)
return flat
[docs]
def export_full_schema(output_dir: str | Path, *, indent: int = 2) -> dict[str, Path]:
"""Write the three companion JSON files into *output_dir*.
Parameters
----------
output_dir
Destination directory (created if missing).
indent
JSON indent used for human-readable output.
Returns
-------
dict[str, Path]
Map keyed by ``config``, ``meta`` and ``validators`` pointing at the
files that were written.
"""
out = Path(output_dir).expanduser().resolve()
out.mkdir(parents=True, exist_ok=True)
schema = export_schema()
meta = build_config_meta()
validators = build_field_validators()
config_path = out / SCHEMA_FILE
meta_path = out / META_FILE
validators_path = out / VALIDATORS_FILE
config_path.write_text(
json.dumps(schema, indent=indent, ensure_ascii=False) + "\n",
encoding="utf-8",
)
meta_path.write_text(
json.dumps(meta, indent=indent, ensure_ascii=False) + "\n",
encoding="utf-8",
)
validators_path.write_text(
json.dumps(validators, indent=indent, ensure_ascii=False, sort_keys=True) + "\n",
encoding="utf-8",
)
return {
"config": config_path,
"meta": meta_path,
"validators": validators_path,
}
__all__ = [
"build_config_meta",
"build_field_validators",
"export_full_schema",
"SCHEMA_FILE",
"META_FILE",
"VALIDATORS_FILE",
]