chore: import upstream snapshot with attribution
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
Validate YAML Workflows / Validate YAML Configuration Files (push) Has been cancelled
This commit is contained in:
Executable
+139
@@ -0,0 +1,139 @@
|
||||
"""Schema exporter for dynamic configuration metadata."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Sequence, Type
|
||||
|
||||
from entity.configs import BaseConfig
|
||||
from entity.configs.graph import DesignConfig
|
||||
|
||||
SCHEMA_VERSION = "0.1.0"
|
||||
|
||||
|
||||
class SchemaResolutionError(ValueError):
|
||||
"""Raised when breadcrumbs fail to resolve to a config node."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Breadcrumb:
|
||||
"""Describes one hop in the config tree."""
|
||||
|
||||
node: str
|
||||
field: str | None = None
|
||||
value: Any | None = None
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, data: Mapping[str, Any]) -> "Breadcrumb":
|
||||
node = str(data.get("node")) if data.get("node") else ""
|
||||
if not node:
|
||||
raise SchemaResolutionError("breadcrumb entry missing 'node'")
|
||||
field = data.get("field")
|
||||
if field is not None:
|
||||
field = str(field)
|
||||
index = data.get("index")
|
||||
if index is not None and not isinstance(index, int):
|
||||
raise SchemaResolutionError("breadcrumb 'index' must be integer when provided")
|
||||
value = data.get("value")
|
||||
return cls(node=node, field=field, value=value)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {"node": self.node}
|
||||
if self.field is not None:
|
||||
payload["field"] = self.field
|
||||
if self.value is not None:
|
||||
payload["value"] = self.value
|
||||
return payload
|
||||
|
||||
|
||||
def _normalize_breadcrumbs(raw: Sequence[Mapping[str, Any]] | None) -> List[Breadcrumb]:
|
||||
if not raw:
|
||||
return []
|
||||
return [Breadcrumb.from_mapping(item) for item in raw]
|
||||
|
||||
|
||||
def _resolve_config_class(
|
||||
breadcrumbs: Sequence[Breadcrumb],
|
||||
*,
|
||||
root_cls: Type[BaseConfig] = DesignConfig,
|
||||
) -> Type[BaseConfig]:
|
||||
current_cls: Type[BaseConfig] = root_cls
|
||||
for crumb in breadcrumbs:
|
||||
if crumb.node != current_cls.__name__:
|
||||
raise SchemaResolutionError(
|
||||
f"breadcrumb node '{crumb.node}' does not match current config '{current_cls.__name__}'"
|
||||
)
|
||||
if crumb.field is None:
|
||||
continue
|
||||
child_cls = current_cls.resolve_child(crumb.field, crumb.value)
|
||||
if child_cls is None:
|
||||
spec = current_cls.field_specs().get(crumb.field)
|
||||
if not spec or spec.child is None:
|
||||
raise SchemaResolutionError(
|
||||
f"field '{crumb.field}' on {current_cls.__name__} is not navigable"
|
||||
)
|
||||
child_cls = spec.child
|
||||
current_cls = child_cls
|
||||
return current_cls
|
||||
|
||||
|
||||
def _serialize_field(config_cls: Type[BaseConfig], name: str, spec_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
field_spec = spec_dict[name]
|
||||
data = field_spec.to_json()
|
||||
routes = [
|
||||
{
|
||||
"childKey": key.to_json(),
|
||||
"childNode": target.__name__,
|
||||
}
|
||||
for key, target in config_cls.child_routes().items()
|
||||
if key.field == name
|
||||
]
|
||||
if routes:
|
||||
data["childRoutes"] = routes
|
||||
return data
|
||||
|
||||
|
||||
def _ordered_field_names(specs: Mapping[str, Any]) -> List[str]:
|
||||
"""Return field names with required ones first while keeping relative order."""
|
||||
|
||||
items = list(specs.items())
|
||||
required_names = [name for name, spec in items if getattr(spec, "required", False)]
|
||||
optional_names = [name for name, spec in items if not getattr(spec, "required", False)]
|
||||
return required_names + optional_names
|
||||
|
||||
|
||||
def _hash_payload(payload: Dict[str, Any]) -> str:
|
||||
serialized = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
|
||||
return hashlib.sha1(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def build_schema_response(
|
||||
breadcrumbs_raw: Sequence[Mapping[str, Any]] | None = None,
|
||||
*,
|
||||
root_cls: Type[BaseConfig] = DesignConfig,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a JSON-serializable schema response for the provided breadcrumbs."""
|
||||
|
||||
breadcrumbs = _normalize_breadcrumbs(breadcrumbs_raw)
|
||||
target_cls = _resolve_config_class(breadcrumbs, root_cls=root_cls)
|
||||
schema_node = target_cls.collect_schema()
|
||||
field_specs = target_cls.field_specs()
|
||||
ordered_fields = _ordered_field_names(field_specs)
|
||||
fields_payload = [_serialize_field(target_cls, name, field_specs) for name in ordered_fields]
|
||||
|
||||
response = {
|
||||
"schemaVersion": SCHEMA_VERSION,
|
||||
"node": schema_node.node,
|
||||
"fields": fields_payload,
|
||||
"constraints": [constraint.to_json() for constraint in schema_node.constraints],
|
||||
"breadcrumbs": [crumb.to_json() for crumb in breadcrumbs],
|
||||
}
|
||||
response["cacheKey"] = _hash_payload({"node": schema_node.node, "breadcrumbs": response["breadcrumbs"]})
|
||||
return response
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Breadcrumb",
|
||||
"SchemaResolutionError",
|
||||
"build_schema_response",
|
||||
]
|
||||
Reference in New Issue
Block a user