"""Сериализация в json-совместимые значения и обратно (для golden-тестов и обмена).""" from collections.abc import Mapping from dataclasses import fields, is_dataclass from datetime import date from decimal import Decimal from enum import Enum from typing import Any from arm_statements.model import ( Cell, CellValue, DocumentType, Evidence, FormCandidate, HeaderCells, Severity, Source, StatementWarning, ) def _value_to_jsonable(value: CellValue) -> Any: """Тег типа значения ячейки; bool проверяется раньше прочего (не подкласс Decimal/date, но явно).""" if value is None: return None if isinstance(value, bool): return {"bool": value} if isinstance(value, Decimal): return {"decimal": str(value)} return {"date": value.isoformat()} def _scalar_to_jsonable(obj: object) -> Any: if isinstance(obj, Enum): return obj.value if isinstance(obj, Decimal): return str(obj) if isinstance(obj, date): return obj.isoformat() return obj def to_jsonable(obj: object) -> Any: """Dataclass пакета → json-совместимое значение.""" if isinstance(obj, Cell): return { "text": obj.text, "value": _value_to_jsonable(obj.value), "confidence": obj.confidence, "ref": obj.ref, } if is_dataclass(obj) and not isinstance(obj, type): return {f.name: to_jsonable(getattr(obj, f.name)) for f in fields(obj)} if isinstance(obj, tuple | list): return [to_jsonable(item) for item in obj] return _scalar_to_jsonable(obj) def _value_from_dict(raw: Mapping[str, Any] | None) -> CellValue: if raw is None: return None if "decimal" in raw: return Decimal(raw["decimal"]) if "date" in raw: return date.fromisoformat(raw["date"]) if "bool" in raw: return bool(raw["bool"]) raise ValueError(f"неизвестный тег значения ячейки: {sorted(raw)}") def _cell_from_dict(raw: Mapping[str, Any] | None) -> Cell | None: if raw is None: return None return Cell( text=raw["text"], value=_value_from_dict(raw["value"]), confidence=float(raw["confidence"]), ref=raw["ref"], ) def _warning_from_dict(raw: Mapping[str, Any]) -> StatementWarning: return StatementWarning( code=raw["code"], severity=Severity(raw["severity"]), message=raw["message"], line_refs=tuple(raw.get("line_refs", ())), column_index=raw.get("column_index"), field=raw.get("field"), locator=raw.get("locator"), ) def candidate_from_dict(data: Mapping[str, Any]) -> FormCandidate: """Обратная к `to_jsonable` для кандидата.""" type_hint = data["type_hint"] evidence = data["evidence"] return FormCandidate( source=Source(data["source"]), locator=data["locator"], type_hint=None if type_hint is None else DocumentType(type_hint), evidence=Evidence(okud=_cell_from_dict(evidence["okud"]), title=_cell_from_dict(evidence["title"])), header=HeaderCells(**{name: _cell_from_dict(raw) for name, raw in data["header"].items()}), rows=tuple(tuple(_cell_from_dict(raw) for raw in row) for row in data["rows"]), segment_breaks=tuple(data.get("segment_breaks", ())), warnings=tuple(_warning_from_dict(raw) for raw in data.get("warnings", ())), )