Module: ui_dict_serializer
Expand source code
# Copyright (C) 2023-present The Project Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from enum import Enum
from typing import Any
from typing import List
from typing_extensions import Dict
from cl.runtime.primitive.case_util import CaseUtil
from cl.runtime.records.protocols import RecordProtocol
from cl.runtime.records.protocols import TDataDict
from cl.runtime.records.protocols import is_key
from cl.runtime.records.record_util import RecordUtil
from cl.runtime.schema.element_decl import ElementDecl
from cl.runtime.schema.type_decl import TypeDecl
from cl.runtime.serialization.dict_serializer import DictSerializer
from cl.runtime.serialization.dict_serializer import _get_class_hierarchy_slots
from cl.runtime.serialization.dict_serializer import get_type_dict
from cl.runtime.serialization.string_serializer import StringSerializer
@dataclass(slots=True, kw_only=True)
class UiDictSerializer(DictSerializer):
"""Serialization for slot-based classes to ui format dict (legacy format)."""
pascalize_keys: bool = True
"""pascalize_keys is True by default."""
def serialize_data(self, data, select_fields: List[str] | None = None):
if not self.pascalize_keys:
raise RuntimeError("Expect ui serialization always with pascalized keys.")
if data is None:
return None
if data.__class__.__name__ in self.primitive_type_names:
return data
elif isinstance(data, Enum):
# Serialize enum as its name
serialized_enum = super(UiDictSerializer, self).serialize_data(data, select_fields)
pascal_case_value = serialized_enum.get("_name")
return pascal_case_value
elif is_key(data):
# Serialize key as string
key_serializer = StringSerializer()
return key_serializer.serialize_key(data)
elif isinstance(data, dict):
# Serialize dict as list of dicts in format [{"key": [key], "value": [value_as_legacy_variant]}]
serialized_dict_items = []
for k, v in super(UiDictSerializer, self).serialize_data(data).items():
# TODO (Roman): support more value types in dict
# Apply custom format for None in dict
if v is None:
serialized_dict_items.append({"key": k, "value": {"Empty": None}})
continue
if isinstance(v, str):
value_type = "String"
elif isinstance(v, int):
value_type = "Int"
elif isinstance(v, float):
value_type = "Double"
else:
raise ValueError(f"Value of type {type(v)} is not supported in dict ui serialization. Value: {v}.")
serialized_dict_items.append({"key": k, "value": {value_type: v}})
return serialized_dict_items
elif getattr(data, "__slots__", None) is not None:
# Invoke 'init' for each class in class hierarchy that implements it, in the order from base to derived
RecordUtil.init_all(data)
serialized_data = super(UiDictSerializer, self).serialize_data(data, select_fields)
# Replace "_type" with "_t"
if "_type" in serialized_data:
serialized_data["_t"] = data.__class__.__name__
del serialized_data["_type"]
serialized_data = {k: v for k, v in serialized_data.items()}
return serialized_data
else:
return super(UiDictSerializer, self).serialize_data(data, select_fields)
def serialize_record_for_table(self, record: RecordProtocol) -> Dict[str, Any]:
"""
Serialize record to ui table format.
Contains only fields of supported types, _key and _t will be added based on record.
"""
key_serializer = StringSerializer()
all_slots = _get_class_hierarchy_slots(record.__class__)
# Get subset of slots which supported in table format
table_slots = [
slot
for slot in all_slots
if (slot_v := getattr(record, slot))
and (
# TODO (Roman): check other types for table format
# select fields if it is primitive, key or enum
slot_v.__class__.__name__ in self.primitive_type_names
or is_key(slot_v)
or isinstance(slot_v, Enum)
)
]
# Serialize record to ui format using table_slots
table_record: Dict[str, Any] = self.serialize_data(record, select_fields=table_slots)
# Replace "_type" with "_t"
if "_type" in table_record:
table_record["_t"] = record.__class__.__name__
del table_record["_type"]
# Add "_key"
table_record["_key"] = key_serializer.serialize_key(record.get_key())
return table_record
def apply_ui_conversion(self, data: TDataDict, element_decl: ElementDecl | None = None) -> TDataDict:
"""
Apply conversion to make ui data serializable. Extract additional info about types from TypeDecl.
element_decl can be None for data with _t on root. Then, for nested fields will be used element decls from
specific TypeDecl object.
"""
if not self.pascalize_keys:
raise RuntimeError("Expect ui serialization always with pascalized keys.")
if isinstance(data, dict):
if (short_name := data.get("_t")) is not None:
# Check _t and create TypeDecl object
type_dict = get_type_dict()
type_ = type_dict.get(short_name) # noqa
type_decl = TypeDecl.for_type(type_)
# Construct name to element decl map
type_decl_elements = (
{
# TODO (Roman): remove extra suffix for elements search after introducing field aliases.
# This is currently needed because ElementDecl removes the _ suffix from the field name.
f"{element.name}{extra_suffix}": element
for element in type_decl.elements
for extra_suffix in ("", "_")
}
if type_decl.elements is not None
else {}
)
# Create empty result with _type attribute (instead of _t)
result = {"_type": short_name}
for field, value in data.items():
if field == "_t":
continue
# Expect pascal case fields
CaseUtil.check_pascal_case(field.removesuffix("_"))
if (field_decl := type_decl_elements.get(field)) is not None:
# Apply ui conversion for values recursively
result[field] = self.apply_ui_conversion(value, field_decl)
else:
# If element decl is not found for field in data raise RuntimeError
raise RuntimeError(
f'Data conflicts with type declaration. Field "{field}" not found '
f'in "{short_name}" type elements.'
)
return result
elif isinstance(data, str):
# Apply ui conversions for string values
if (enum := element_decl.enum) is not None:
# Get enum type from element decl and convert value to dict supported by DictSerializer
enum_type_name = enum.name
return {"_enum": enum_type_name, "_name": CaseUtil.upper_to_pascal_case(data)}
elif (key := element_decl.key_) is not None:
# Get key type from element decl
key_type_name = key.name
type_dict = get_type_dict()
key_type = type_dict.get(key_type_name) # noqa
# Deserialize key from string
key_serializer = StringSerializer()
result = key_serializer.deserialize_key(data, key_type)
return result
elif hasattr(data, "__iter__"):
# Apply ui conversion for each element in iterable
return [self.apply_ui_conversion(x, element_decl) for x in data] # noqa
# Return unchanged data if there is no ui conversion
return data
Classes
class UiDictSerializer (*, pascalize_keys: bool = True)
-
Serialization for slot-based classes to ui format dict (legacy format).
Expand source code
@dataclass(slots=True, kw_only=True) class UiDictSerializer(DictSerializer): """Serialization for slot-based classes to ui format dict (legacy format).""" pascalize_keys: bool = True """pascalize_keys is True by default.""" def serialize_data(self, data, select_fields: List[str] | None = None): if not self.pascalize_keys: raise RuntimeError("Expect ui serialization always with pascalized keys.") if data is None: return None if data.__class__.__name__ in self.primitive_type_names: return data elif isinstance(data, Enum): # Serialize enum as its name serialized_enum = super(UiDictSerializer, self).serialize_data(data, select_fields) pascal_case_value = serialized_enum.get("_name") return pascal_case_value elif is_key(data): # Serialize key as string key_serializer = StringSerializer() return key_serializer.serialize_key(data) elif isinstance(data, dict): # Serialize dict as list of dicts in format [{"key": [key], "value": [value_as_legacy_variant]}] serialized_dict_items = [] for k, v in super(UiDictSerializer, self).serialize_data(data).items(): # TODO (Roman): support more value types in dict # Apply custom format for None in dict if v is None: serialized_dict_items.append({"key": k, "value": {"Empty": None}}) continue if isinstance(v, str): value_type = "String" elif isinstance(v, int): value_type = "Int" elif isinstance(v, float): value_type = "Double" else: raise ValueError(f"Value of type {type(v)} is not supported in dict ui serialization. Value: {v}.") serialized_dict_items.append({"key": k, "value": {value_type: v}}) return serialized_dict_items elif getattr(data, "__slots__", None) is not None: # Invoke 'init' for each class in class hierarchy that implements it, in the order from base to derived RecordUtil.init_all(data) serialized_data = super(UiDictSerializer, self).serialize_data(data, select_fields) # Replace "_type" with "_t" if "_type" in serialized_data: serialized_data["_t"] = data.__class__.__name__ del serialized_data["_type"] serialized_data = {k: v for k, v in serialized_data.items()} return serialized_data else: return super(UiDictSerializer, self).serialize_data(data, select_fields) def serialize_record_for_table(self, record: RecordProtocol) -> Dict[str, Any]: """ Serialize record to ui table format. Contains only fields of supported types, _key and _t will be added based on record. """ key_serializer = StringSerializer() all_slots = _get_class_hierarchy_slots(record.__class__) # Get subset of slots which supported in table format table_slots = [ slot for slot in all_slots if (slot_v := getattr(record, slot)) and ( # TODO (Roman): check other types for table format # select fields if it is primitive, key or enum slot_v.__class__.__name__ in self.primitive_type_names or is_key(slot_v) or isinstance(slot_v, Enum) ) ] # Serialize record to ui format using table_slots table_record: Dict[str, Any] = self.serialize_data(record, select_fields=table_slots) # Replace "_type" with "_t" if "_type" in table_record: table_record["_t"] = record.__class__.__name__ del table_record["_type"] # Add "_key" table_record["_key"] = key_serializer.serialize_key(record.get_key()) return table_record def apply_ui_conversion(self, data: TDataDict, element_decl: ElementDecl | None = None) -> TDataDict: """ Apply conversion to make ui data serializable. Extract additional info about types from TypeDecl. element_decl can be None for data with _t on root. Then, for nested fields will be used element decls from specific TypeDecl object. """ if not self.pascalize_keys: raise RuntimeError("Expect ui serialization always with pascalized keys.") if isinstance(data, dict): if (short_name := data.get("_t")) is not None: # Check _t and create TypeDecl object type_dict = get_type_dict() type_ = type_dict.get(short_name) # noqa type_decl = TypeDecl.for_type(type_) # Construct name to element decl map type_decl_elements = ( { # TODO (Roman): remove extra suffix for elements search after introducing field aliases. # This is currently needed because ElementDecl removes the _ suffix from the field name. f"{element.name}{extra_suffix}": element for element in type_decl.elements for extra_suffix in ("", "_") } if type_decl.elements is not None else {} ) # Create empty result with _type attribute (instead of _t) result = {"_type": short_name} for field, value in data.items(): if field == "_t": continue # Expect pascal case fields CaseUtil.check_pascal_case(field.removesuffix("_")) if (field_decl := type_decl_elements.get(field)) is not None: # Apply ui conversion for values recursively result[field] = self.apply_ui_conversion(value, field_decl) else: # If element decl is not found for field in data raise RuntimeError raise RuntimeError( f'Data conflicts with type declaration. Field "{field}" not found ' f'in "{short_name}" type elements.' ) return result elif isinstance(data, str): # Apply ui conversions for string values if (enum := element_decl.enum) is not None: # Get enum type from element decl and convert value to dict supported by DictSerializer enum_type_name = enum.name return {"_enum": enum_type_name, "_name": CaseUtil.upper_to_pascal_case(data)} elif (key := element_decl.key_) is not None: # Get key type from element decl key_type_name = key.name type_dict = get_type_dict() key_type = type_dict.get(key_type_name) # noqa # Deserialize key from string key_serializer = StringSerializer() result = key_serializer.deserialize_key(data, key_type) return result elif hasattr(data, "__iter__"): # Apply ui conversion for each element in iterable return [self.apply_ui_conversion(x, element_decl) for x in data] # noqa # Return unchanged data if there is no ui conversion return data
Ancestors
Class variables
var primitive_type_names
-
Inherited from:
DictSerializer
.primitive_type_names
Detect primitive type by checking if class name is in this list.
Fields
var pascalize_keys -> bool
-
Inherited from:
DictSerializer
.pascalize_keys
If true, pascalize keys during serialization.
Methods
def apply_ui_conversion(self, data: Dict[str, Union[Dict[str, ForwardRef('TDataField')], List[ForwardRef('TDataField')], str, float, bool, int, datetime.date, datetime.time, datetime.datetime, uuid.UUID, bytes, ForwardRef(None), enum.Enum]], element_decl: ElementDecl | None = None)
-
Apply conversion to make ui data serializable. Extract additional info about types from TypeDecl.
element_decl can be None for data with _t on root. Then, for nested fields will be used element decls from specific TypeDecl object.
def deserialize_data(self, data: Dict[str, Union[Dict[str, ForwardRef('TDataField')], List[ForwardRef('TDataField')], str, float, bool, int, datetime.date, datetime.time, datetime.datetime, uuid.UUID, bytes, ForwardRef(None), enum.Enum]])
-
Inherited from:
DictSerializer
.deserialize_data
Deserialize object from data, invoke init_all after deserialization.
def serialize_data(self, data, select_fields: Optional[List[str]] = None)
-
Inherited from:
DictSerializer
.serialize_data
Serialize to dictionary containing primitive types, dictionaries, or iterables …
def serialize_record_for_table(self, record: RecordProtocol) -> Dict[str, Any]
-
Serialize record to ui table format. Contains only fields of supported types, _key and _t will be added based on record.