Source code for buffalo_core.schema

"""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


[docs] @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)
[docs] @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)
[docs] def to_metadata(self) -> SchemaFieldMetadata: """Return the plain metadata mapping stored in dataclass fields.""" return { "value": self.value, "label": self.label, }
[docs] @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."""
[docs] 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
[docs] @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."""
[docs] @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."""
[docs] @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."""
[docs] 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
[docs] def schema_field_metadata(cls: type[object]) -> dict[str, SchemaFieldMetadata]: """ Return structured metadata for the dataclass fields on ``cls``. Parameters ---------- cls : type[object] Dataclass type whose field metadata should be inspected. Returns ------- dict[str, SchemaFieldMetadata] Ordered mapping from field name to the structured metadata stored on that dataclass field. Raises ------ TypeError If ``cls`` is not a dataclass type. """ if not is_dataclass(cls): msg = "cls must be a dataclass type." raise TypeError(msg) return { field_info.name: cast(SchemaFieldMetadata, dict(field_info.metadata)) for field_info in fields(cls) if field_info.metadata }
[docs] 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), )