Serializer: use alias, make it configurable for different use cases

This commit is contained in:
Johannes Kirschbauer
2024-07-29 09:00:24 +02:00
parent d53ac276a7
commit 395a7fc70e
2 changed files with 40 additions and 30 deletions

View File

@@ -49,32 +49,35 @@ def sanitize_string(s: str) -> str:
return json.dumps(s)[1:-1] return json.dumps(s)[1:-1]
def dataclass_to_dict(obj: Any) -> Any: def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any:
""" def _to_dict(obj: Any) -> Any:
Utility function to convert dataclasses to dictionaries """
It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries Utility function to convert dataclasses to dictionaries
It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries
It does NOT convert member functions. It does NOT convert member functions.
""" """
if is_dataclass(obj): if is_dataclass(obj):
return { return {
# Use either the original name or name # Use either the original name or name
sanitize_string( sanitize_string(
field.metadata.get("original_name", field.name) field.metadata.get("alias", field.name) if use_alias else field.name
): dataclass_to_dict(getattr(obj, field.name)) ): _to_dict(getattr(obj, field.name))
for field in fields(obj) for field in fields(obj)
if not field.name.startswith("_") # type: ignore if not field.name.startswith("_") # type: ignore
} }
elif isinstance(obj, list | tuple): elif isinstance(obj, list | tuple):
return [dataclass_to_dict(item) for item in obj] return [_to_dict(item) for item in obj]
elif isinstance(obj, dict): elif isinstance(obj, dict):
return {sanitize_string(k): dataclass_to_dict(v) for k, v in obj.items()} return {sanitize_string(k): _to_dict(v) for k, v in obj.items()}
elif isinstance(obj, Path): elif isinstance(obj, Path):
return sanitize_string(str(obj)) return sanitize_string(str(obj))
elif isinstance(obj, str): elif isinstance(obj, str):
return sanitize_string(obj) return sanitize_string(obj)
else: else:
return obj return obj
return _to_dict(obj)
T = TypeVar("T", bound=dataclass) # type: ignore T = TypeVar("T", bound=dataclass) # type: ignore

View File

@@ -4,10 +4,7 @@ from pathlib import Path
import pytest import pytest
# Functions to test # Functions to test
from clan_cli.api import ( from clan_cli.api import dataclass_to_dict, from_dict
dataclass_to_dict,
from_dict,
)
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.inventory import ( from clan_cli.inventory import (
Inventory, Inventory,
@@ -87,6 +84,7 @@ def test_simple_field_missing() -> None:
def test_deserialize_extensive_inventory() -> None: def test_deserialize_extensive_inventory() -> None:
# TODO: Make this an abstract test, so it doesn't break the test if the inventory changes
data = { data = {
"meta": {"name": "superclan", "description": "nice clan"}, "meta": {"name": "superclan", "description": "nice clan"},
"services": { "services": {
@@ -130,7 +128,16 @@ def test_alias_field() -> None:
data = {"--user-name--": "John"} data = {"--user-name--": "John"}
expected = Person(name="John") expected = Person(name="John")
assert from_dict(Person, data) == expected person = from_dict(Person, data)
# Deserialize
assert person == expected
# Serialize with alias
assert dataclass_to_dict(person) == data
# Serialize without alias
assert dataclass_to_dict(person, use_alias=False) == {"name": "John"}
def test_path_field() -> None: def test_path_field() -> None: