"""Structured schema metadata shared across Buffalo projects."""
from __future__ import annotations
import re
from dataclasses import dataclass, fields, is_dataclass
from types import NoneType, UnionType
from typing import Any, Union, cast, get_args, get_origin, get_type_hints
__all__ = [
"SchemaChoice",
"SchemaClassTree",
"SchemaCondition",
"SchemaField",
"SchemaFieldMetadata",
"SchemaFieldTree",
"schema_field_metadata",
"schema_tree",
]
type SchemaFieldMetadata = dict[str, object]
_PAIR_ARG_COUNT = 2
@dataclass(frozen=True, slots=True)
class SchemaChoice:
"""
Structured choice metadata for one enum-like schema value.
Notes
-----
``value`` remains the serialized schema token.
``label`` provides the human-readable text that GUIs or editors can
display without changing the underlying serialized contract.
"""
value: object
"""Serialized schema value for one choice."""
label: str
"""Human-readable label for presenting the choice in a UI."""
def __post_init__(self) -> None:
"""Validate that one display label is present."""
if not self.label:
msg = "SchemaChoice label must be a non-empty string."
raise ValueError(msg)
@classmethod
def from_value(
cls,
value: object,
*,
label: str | None = None,
) -> SchemaChoice:
"""
Build one choice from a serialized value.
Parameters
----------
value : object
Serialized schema value to preserve in metadata.
label : str | None, optional
Optional explicit display label.
When omitted, a generic display label is derived from ``value``.
Returns
-------
SchemaChoice
Structured choice metadata carrying both the serialized value and
a display-friendly label.
"""
if label is None:
label = _default_choice_label(value)
return cls(value=value, label=label)
def to_metadata(self) -> SchemaFieldMetadata:
"""Return the plain metadata mapping stored in dataclass fields."""
return {
"value": self.value,
"label": self.label,
}
@dataclass(frozen=True, slots=True)
class SchemaCondition:
"""
Structured conditional UI rule attached to one schema field.
Notes
-----
Conditions remain domain-light so downstream projects can interpret them
for GUI visibility, editability, or other editor workflows.
"""
path: str
"""Path expression identifying the controlling schema value."""
operator: str
"""Predicate operator such as ``equals`` or ``non_empty``."""
value: object | None = None
"""Optional comparison value for the condition."""
message: str | None = None
"""Optional explanatory message for the condition."""
def to_metadata(self) -> SchemaFieldMetadata:
"""Return the plain metadata mapping stored in dataclass fields."""
metadata: SchemaFieldMetadata = {
"path": self.path,
"operator": self.operator,
}
_store_optional(metadata, key="value", value=self.value)
_store_optional(metadata, key="message", value=self.message)
return metadata
@dataclass(frozen=True, slots=True)
class SchemaFieldTree:
"""
Tree node describing one dataclass field.
Notes
-----
This object keeps the structure domain-light.
It reports only the field shape, nested dataclass children, optionality,
and the stored field metadata contract.
"""
name: str
"""Field name on the parent dataclass."""
type_name: str
"""Display-friendly type name for the field."""
optional: bool
"""Whether the field accepts ``None``."""
docstring: str | None
"""Best available short field documentation text."""
metadata: SchemaFieldMetadata
"""Structured machine-readable field metadata."""
object_schema: SchemaClassTree | None = None
"""Nested dataclass schema when the field value is a dataclass object."""
item_type_name: str | None = None
"""Display-friendly item type name for list, tuple, or mapping values."""
item_schema: SchemaClassTree | None = None
"""Nested schema for collection items when the item type is a dataclass."""
@dataclass(frozen=True, slots=True)
class SchemaClassTree:
"""Tree node describing one schema dataclass."""
class_name: str
"""Dataclass name."""
docstring: str | None
"""Top-level class documentation string."""
fields: tuple[SchemaFieldTree, ...]
"""Ordered field descriptions."""
@dataclass(frozen=True, slots=True)
class SchemaField:
"""
Structured machine-readable metadata for one dataclass field.
Notes
-----
This dataclass is a builder for the plain dictionary stored in
``dataclasses.field(metadata=...)``.
Downstream projects keep their dataclasses as the source of truth while
still attaching stable GUI and editor hints in one shared Core format.
"""
value_kind: str
"""Machine-readable category such as ``enum``, ``number``, or ``list``."""
required: bool
"""Whether the field is required by the schema contract."""
label: str
"""Human-readable field label for UIs and editors."""
order: int
"""Suggested stable display ordering for sibling fields."""
short_help: str
"""Short help text describing the field for direct UI display."""
default: object | None = None
"""Optional default value when one is meaningful for editing."""
choices: tuple[SchemaChoice | object, ...] | None = None
"""Optional enum-like choices exposed as structured value/label pairs."""
minimum: float | int | None = None
"""Inclusive numeric lower bound when one is known."""
maximum: float | int | None = None
"""Inclusive numeric upper bound when one is known."""
exclusive_minimum: float | int | None = None
"""Exclusive numeric lower bound when one is known."""
exclusive_maximum: float | int | None = None
"""Exclusive numeric upper bound when one is known."""
tuple_length: int | None = None
"""Expected tuple length when the field stores a fixed-size tuple."""
min_items: int | None = None
"""Minimum item count when the field stores a collection."""
max_items: int | None = None
"""Maximum item count when the field stores a collection."""
item_kind: str | None = None
"""Machine-readable description of collection item shape or type."""
format_hint: str | None = None
"""Optional formatting hint for text or tuple entry widgets."""
reference_target: str | None = None
"""Optional logical target when the field references named objects."""
collection_add_label: str | None = None
"""Optional add-item button label for collection editors."""
collection_item_label_field: str | None = None
"""Optional item field used as the row label in collection editors."""
collection_item_creation_hint: str | None = None
"""Optional help text for creating new collection items."""
collection_key_label: str | None = None
"""Optional label used for mapping keys in collection editors."""
collection_key_format_hint: str | None = None
"""Optional format hint used for mapping keys in collection editors."""
notes: str | None = None
"""Optional extra machine-usable clarification text."""
advanced: bool = False
"""Whether the field should default to an advanced editing section."""
shown_when: tuple[SchemaCondition, ...] | None = None
"""Optional conditions controlling whether a field should be shown."""
enabled_when: tuple[SchemaCondition, ...] | None = None
"""Optional conditions controlling whether a field should be editable."""
required_when: tuple[SchemaCondition, ...] | None = None
"""Optional conditions controlling whether a field becomes required."""
def to_metadata(self) -> SchemaFieldMetadata:
"""Return the plain metadata mapping stored in dataclass fields."""
metadata: SchemaFieldMetadata = {
"value_kind": self.value_kind,
"required": self.required,
"label": self.label,
"order": self.order,
"short_help": self.short_help,
"advanced": self.advanced,
}
_store_optional(metadata, key="default", value=self.default)
_store_optional(metadata, key="minimum", value=self.minimum)
_store_optional(metadata, key="maximum", value=self.maximum)
_store_optional(
metadata,
key="exclusive_minimum",
value=self.exclusive_minimum,
)
_store_optional(
metadata,
key="exclusive_maximum",
value=self.exclusive_maximum,
)
_store_optional(metadata, key="tuple_length", value=self.tuple_length)
_store_optional(metadata, key="min_items", value=self.min_items)
_store_optional(metadata, key="max_items", value=self.max_items)
_store_optional(metadata, key="item_kind", value=self.item_kind)
_store_optional(metadata, key="format_hint", value=self.format_hint)
_store_optional(
metadata,
key="reference_target",
value=self.reference_target,
)
_store_optional(
metadata,
key="collection_add_label",
value=self.collection_add_label,
)
_store_optional(
metadata,
key="collection_item_label_field",
value=self.collection_item_label_field,
)
_store_optional(
metadata,
key="collection_item_creation_hint",
value=self.collection_item_creation_hint,
)
_store_optional(
metadata,
key="collection_key_label",
value=self.collection_key_label,
)
_store_optional(
metadata,
key="collection_key_format_hint",
value=self.collection_key_format_hint,
)
_store_optional(metadata, key="notes", value=self.notes)
_store_conditions(
metadata,
key="shown_when",
conditions=self.shown_when,
)
_store_conditions(
metadata,
key="enabled_when",
conditions=self.enabled_when,
)
_store_conditions(
metadata,
key="required_when",
conditions=self.required_when,
)
if self.choices is not None:
metadata["choices"] = tuple(
_normalize_choice(choice).to_metadata()
for choice in self.choices
)
return metadata
TOKENIZED_CHOICE_LABELS: dict[str, str] = {
"cfd": "CFD",
"cst": "CST",
"id": "ID",
"naca": "NACA",
"nasa": "NASA",
"parsec": "PARSEC",
"vlm": "VLM",
}
def _default_choice_label(value: object) -> str:
"""Return a generic display label for one serialized choice value."""
if isinstance(value, str):
words = value.strip().replace("_", " ").split()
if not words:
return value
return " ".join(_label_word(word) for word in words)
return str(value)
def _label_word(word: str) -> str:
"""Return one display-friendly token inside a derived choice label."""
normalized = word.lower()
if normalized in TOKENIZED_CHOICE_LABELS:
return TOKENIZED_CHOICE_LABELS[normalized]
if re.fullmatch(r"[A-Za-z]+\d+[A-Za-z]*", word):
return word.upper()
return word.title()
def _normalize_choice(choice: SchemaChoice | object) -> SchemaChoice:
"""Return one normalized structured choice entry."""
if isinstance(choice, SchemaChoice):
return choice
return SchemaChoice.from_value(choice)
def _unwrap_optional(target_type: Any) -> tuple[object, bool]:
"""Return the non-optional type plus whether ``None`` is allowed."""
origin = get_origin(target_type)
if origin is UnionType or origin is Union:
args = tuple(
arg for arg in get_args(target_type) if arg is not NoneType
)
if len(args) + 1 == len(get_args(target_type)):
if len(args) == 1:
return args[0], True
return target_type, True
return target_type, False
def _type_name(target_type: Any) -> str:
"""Return a display-friendly type name."""
origin = get_origin(target_type)
if origin is None:
if isinstance(target_type, type):
return target_type.__name__
return str(target_type)
args = get_args(target_type)
origin_name = getattr(origin, "__name__", str(origin))
if args:
return f"{origin_name}[{', '.join(_type_name(arg) for arg in args)}]"
return origin_name
def _field_docstring(metadata: SchemaFieldMetadata) -> str | None:
"""Return the best available short doc text for a field."""
short_help = metadata.get("short_help")
notes = metadata.get("notes")
if isinstance(short_help, str) and isinstance(notes, str):
return f"{short_help} {notes}"
if isinstance(short_help, str):
return short_help
if isinstance(notes, str):
return notes
return None
def _item_schema_for_type(
target_type: Any,
) -> tuple[str | None, SchemaClassTree | None]:
"""Return item type display name and nested schema when available."""
origin = get_origin(target_type)
args = get_args(target_type)
if origin is list and args:
item_type = args[0]
return _type_name(item_type), (
schema_tree(cast(type[object], item_type))
if isinstance(item_type, type) and is_dataclass(item_type)
else None
)
if origin is tuple and args:
item_type = (
args[0]
if len(args) == _PAIR_ARG_COUNT and args[1] is Ellipsis
else None
)
if item_type is not None:
return _type_name(item_type), (
schema_tree(cast(type[object], item_type))
if isinstance(item_type, type) and is_dataclass(item_type)
else None
)
return "mixed", None
if origin is dict and len(args) == _PAIR_ARG_COUNT:
item_type = args[1]
return _type_name(item_type), (
schema_tree(cast(type[object], item_type))
if isinstance(item_type, type) and is_dataclass(item_type)
else None
)
return None, None
def _store_conditions(
metadata: SchemaFieldMetadata,
*,
key: str,
conditions: tuple[SchemaCondition, ...] | None,
) -> None:
"""Store one condition list when conditions are present."""
if conditions is None:
return
metadata[key] = tuple(condition.to_metadata() for condition in conditions)
def _store_optional(
metadata: SchemaFieldMetadata,
*,
key: str,
value: object | None,
) -> None:
"""Store one metadata value when it is not ``None``."""
if value is not None:
metadata[key] = value
def schema_tree(cls: type[object]) -> SchemaClassTree:
"""
Return a nested tree description for one schema dataclass.
Parameters
----------
cls : type[object]
Dataclass type whose nested field structure should be described.
Returns
-------
SchemaClassTree
Nested field tree carrying ordered field descriptions, optionality,
nested dataclass children, display-friendly type names, and the
stored field metadata contract.
Raises
------
TypeError
If ``cls`` is not a dataclass type.
"""
if not is_dataclass(cls):
msg = "cls must be a dataclass type."
raise TypeError(msg)
type_hints = get_type_hints(cls)
field_trees: list[SchemaFieldTree] = []
for field_info in fields(cls):
annotated_type = type_hints[field_info.name]
value_type, optional = _unwrap_optional(annotated_type)
resolved_value_type = value_type
metadata = cast(SchemaFieldMetadata, dict(field_info.metadata))
object_schema = (
schema_tree(cast(type[object], resolved_value_type))
if isinstance(resolved_value_type, type)
and is_dataclass(resolved_value_type)
else None
)
item_type_name, item_schema = _item_schema_for_type(resolved_value_type)
field_trees.append(
SchemaFieldTree(
name=field_info.name,
type_name=_type_name(resolved_value_type),
optional=optional,
docstring=_field_docstring(metadata),
metadata=metadata,
object_schema=object_schema,
item_type_name=item_type_name,
item_schema=item_schema,
)
)
return SchemaClassTree(
class_name=cls.__name__,
docstring=cls.__doc__.strip() if cls.__doc__ is not None else None,
fields=tuple(field_trees),
)