From dc998df37a2ba37fa43d10c8a1ce044a5b9f5b1e Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sun, 1 Oct 2023 11:42:14 +0200 Subject: [PATCH] feat: format specific (de)serialize (#25) Added functionality to implement custom (de)serialization specific for XML or JSON ([#13](https://github.com/madpah/serializable/issues/13)). Changed ------------- * Class `BaseHelper` is no longer abstract. This class does not provide any functionality, it is more like a Protocol with some fallback implementations. * Method `BaseHelper.serialize()` is no longer abstract. Will raise `NotImplementedError` per default. * Method `BaseHelper.deserialize()` is no longer abstract. Will raise `NotImplementedError` per default. Added ---------- * New method `BaseHelper.json_serialize()` predefined. Will call `cls.serialize()` per default. * New method `BaseHelper.json_deserialize()` predefined. Will call `cls.deserialize()` per default. ---- Signed-off-by: Jan Kowalleck --- serializable/__init__.py | 14 ++--- serializable/helpers.py | 55 +++++++++++++++---- .../the-phoenix-project-camel-case-1-v1.xml | 2 +- .../the-phoenix-project-camel-case-1-v2.xml | 2 +- .../the-phoenix-project-camel-case-1-v3.xml | 2 +- .../the-phoenix-project-camel-case-1-v4.xml | 2 +- .../the-phoenix-project-camel-case-1.xml | 2 +- ...phoenix-project-camel-case-references.json | 2 +- .../the-phoenix-project-camel-case-v2.json | 2 +- .../the-phoenix-project-camel-case-v3.json | 2 +- .../the-phoenix-project-camel-case-v4.json | 2 +- ...oenix-project-camel-case-with-ignored.json | 2 +- ...hoenix-project-camel-case-with-ignored.xml | 2 +- .../the-phoenix-project-camel-case.json | 2 +- .../the-phoenix-project-kebab-case-1-v2.xml | 2 +- .../the-phoenix-project-kebab-case-1.xml | 2 +- .../the-phoenix-project-kebab-case.json | 2 +- .../the-phoenix-project-snake-case-1.xml | 2 +- .../the-phoenix-project-snake-case.json | 2 +- tests/model.py | 21 +++++++ 20 files changed, 90 insertions(+), 34 deletions(-) diff --git a/serializable/__init__.py b/serializable/__init__.py index 848b450..d843eea 100644 --- a/serializable/__init__.py +++ b/serializable/__init__.py @@ -184,7 +184,7 @@ def default(self, o: Any) -> Any: if prop_info.custom_type: if prop_info.is_helper_type(): - v = prop_info.custom_type.serialize(v) + v = prop_info.custom_type.json_serialize(v) else: v = prop_info.custom_type(v) elif prop_info.is_array: @@ -285,7 +285,7 @@ def _from_json(cls: Type[_T], data: Dict[str, Any]) -> object: try: if prop_info.custom_type: if prop_info.is_helper_type(): - _data[k] = prop_info.custom_type.deserialize(v) + _data[k] = prop_info.custom_type.json_deserialize(v) else: _data[k] = prop_info.custom_type(v) elif prop_info.is_array: @@ -348,7 +348,7 @@ def _as_xml(self: _T, view_: Optional[Type[_T]] = None, as_string: bool = True, new_key = CurrentFormatter.formatter.encode(property_name=new_key) if prop_info.custom_type and prop_info.is_helper_type(): - v = prop_info.custom_type.serialize(v) + v = prop_info.custom_type.xml_serialize(v) elif prop_info.is_enum: v = v.value @@ -412,7 +412,7 @@ def _as_xml(self: _T, view_: Optional[Type[_T]] = None, as_string: bool = True, SubElement(nested_e, nested_key).text = str(j) elif prop_info.custom_type: if prop_info.is_helper_type(): - SubElement(this_e, new_key).text = str(prop_info.custom_type.serialize(v)) + SubElement(this_e, new_key).text = str(prop_info.custom_type.xml_serialize(v)) else: SubElement(this_e, new_key).text = str(prop_info.custom_type(v)) elif prop_info.is_enum: @@ -488,7 +488,7 @@ def strip_default_namespace(s: str) -> str: f'{cls.__module__}.{cls.__qualname__} which has Prop Metadata: {prop_info}') if prop_info.custom_type and prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.deserialize(v) + _data[decoded_k] = prop_info.custom_type.xml_deserialize(v) elif prop_info.is_enum: _data[decoded_k] = prop_info.concrete_type(v) elif prop_info.is_primitive_type(): @@ -553,14 +553,14 @@ def strip_default_namespace(s: str) -> str: ) elif prop_info.custom_type: if prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.deserialize(child_e) + _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e) else: _data[decoded_k] = prop_info.custom_type(child_e.text) else: _data[decoded_k].append(prop_info.concrete_type(child_e.text)) elif prop_info.custom_type: if prop_info.is_helper_type(): - _data[decoded_k] = prop_info.custom_type.deserialize(child_e.text) + _data[decoded_k] = prop_info.custom_type.xml_deserialize(child_e.text) else: _data[decoded_k] = prop_info.custom_type(child_e.text) elif prop_info.is_enum: diff --git a/serializable/helpers.py b/serializable/helpers.py index c678f62..198a7fe 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -18,25 +18,60 @@ # Copyright (c) Paul Horton. All Rights Reserved. import re import warnings -from abc import ABC, abstractmethod from datetime import date, datetime from typing import Any -class BaseHelper(ABC): +class BaseHelper: + """Base Helper. - def __init__(self, *args: Any, **kwargs: Any) -> None: - pass + Inherit from this class and implement the needed functions! + + This class does not provide any functionality, + it is more like a Protocol with some fallback implementations. + """ + + # region general/fallback + + @classmethod + def serialize(cls, o: Any) -> Any: + """general purpose serializer""" + raise NotImplementedError() + + @classmethod + def deserialize(cls, o: Any) -> Any: + """general purpose deserializer""" + raise NotImplementedError() + + # endregion general/fallback + + # region json specific @classmethod - @abstractmethod - def serialize(cls, o: object) -> Any: - raise NotImplementedError + def json_serialize(cls, o: Any) -> Any: + """json specific serializer""" + return cls.serialize(o) @classmethod - @abstractmethod - def deserialize(cls, o: str) -> Any: - raise NotImplementedError + def json_deserialize(cls, o: Any) -> Any: + """json specific deserializer""" + return cls.deserialize(o) + + # endregion json specific + + # endregion xml specific + + @classmethod + def xml_serialize(cls, o: Any) -> Any: + """xml specific serializer""" + return cls.serialize(o) + + @classmethod + def xml_deserialize(cls, o: Any) -> Any: + """xml specific deserializer""" + return cls.deserialize(o) + + # endregion xml specific class Iso8601Date(BaseHelper): diff --git a/tests/fixtures/the-phoenix-project-camel-case-1-v1.xml b/tests/fixtures/the-phoenix-project-camel-case-1-v1.xml index b37fef8..f1eae01 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-1-v1.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-1-v1.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case-1-v2.xml b/tests/fixtures/the-phoenix-project-camel-case-1-v2.xml index a7ab9bd..8b168c4 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-1-v2.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-1-v2.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case-1-v3.xml b/tests/fixtures/the-phoenix-project-camel-case-1-v3.xml index 1247a5d..896cc9b 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-1-v3.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-1-v3.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case-1-v4.xml b/tests/fixtures/the-phoenix-project-camel-case-1-v4.xml index aa44d96..846f82c 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-1-v4.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-1-v4.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case-1.xml b/tests/fixtures/the-phoenix-project-camel-case-1.xml index 1717836..4cb8003 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-1.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-1.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case-references.json b/tests/fixtures/the-phoenix-project-camel-case-references.json index 01a5fa4..4eabd6f 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-references.json +++ b/tests/fixtures/the-phoenix-project-camel-case-references.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-camel-case-v2.json b/tests/fixtures/the-phoenix-project-camel-case-v2.json index daf5f5f..1eb94d0 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-v2.json +++ b/tests/fixtures/the-phoenix-project-camel-case-v2.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-camel-case-v3.json b/tests/fixtures/the-phoenix-project-camel-case-v3.json index 91e6516..baf8381 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-v3.json +++ b/tests/fixtures/the-phoenix-project-camel-case-v3.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-camel-case-v4.json b/tests/fixtures/the-phoenix-project-camel-case-v4.json index 01a5fa4..4eabd6f 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-v4.json +++ b/tests/fixtures/the-phoenix-project-camel-case-v4.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-camel-case-with-ignored.json b/tests/fixtures/the-phoenix-project-camel-case-with-ignored.json index 0c48b12..c2a3bc2 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-with-ignored.json +++ b/tests/fixtures/the-phoenix-project-camel-case-with-ignored.json @@ -1,6 +1,6 @@ { "something_to_be_ignored": "some_value", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-camel-case-with-ignored.xml b/tests/fixtures/the-phoenix-project-camel-case-with-ignored.xml index 297244a..017d98a 100644 --- a/tests/fixtures/the-phoenix-project-camel-case-with-ignored.xml +++ b/tests/fixtures/the-phoenix-project-camel-case-with-ignored.xml @@ -1,7 +1,7 @@ thing - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-camel-case.json b/tests/fixtures/the-phoenix-project-camel-case.json index 0eb4c7d..4cd7880 100644 --- a/tests/fixtures/the-phoenix-project-camel-case.json +++ b/tests/fixtures/the-phoenix-project-camel-case.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbnNumber": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-kebab-case-1-v2.xml b/tests/fixtures/the-phoenix-project-kebab-case-1-v2.xml index f04cbb5..62c9b10 100644 --- a/tests/fixtures/the-phoenix-project-kebab-case-1-v2.xml +++ b/tests/fixtures/the-phoenix-project-kebab-case-1-v2.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-kebab-case-1.xml b/tests/fixtures/the-phoenix-project-kebab-case-1.xml index 214b687..e001ae0 100644 --- a/tests/fixtures/the-phoenix-project-kebab-case-1.xml +++ b/tests/fixtures/the-phoenix-project-kebab-case-1.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-kebab-case.json b/tests/fixtures/the-phoenix-project-kebab-case.json index 1e8a00a..6194c75 100644 --- a/tests/fixtures/the-phoenix-project-kebab-case.json +++ b/tests/fixtures/the-phoenix-project-kebab-case.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbn-number": "978-1942788294", "edition": { "number": 5, diff --git a/tests/fixtures/the-phoenix-project-snake-case-1.xml b/tests/fixtures/the-phoenix-project-snake-case-1.xml index 1ea6d13..76711aa 100644 --- a/tests/fixtures/the-phoenix-project-snake-case-1.xml +++ b/tests/fixtures/the-phoenix-project-snake-case-1.xml @@ -1,7 +1,7 @@ f3758bf0-0ff7-4366-a5e5-c209d4352b2d - The Phoenix Project + {X} The Phoenix Project 5th Anniversary Limited Edition 2018-04-16 Kevin Behr diff --git a/tests/fixtures/the-phoenix-project-snake-case.json b/tests/fixtures/the-phoenix-project-snake-case.json index e1faf80..6403bb5 100644 --- a/tests/fixtures/the-phoenix-project-snake-case.json +++ b/tests/fixtures/the-phoenix-project-snake-case.json @@ -1,6 +1,6 @@ { "id": "f3758bf0-0ff7-4366-a5e5-c209d4352b2d", - "title": "The Phoenix Project", + "title": "{J} The Phoenix Project", "isbn_number": "978-1942788294", "edition": { "number": 5, diff --git a/tests/model.py b/tests/model.py index 8025ed5..926faa4 100644 --- a/tests/model.py +++ b/tests/model.py @@ -1,4 +1,5 @@ # encoding: utf-8 +import re # This file is part of py-serializable # @@ -68,6 +69,25 @@ def deserialize(cls, o: object) -> Set["BookReference"]: raise ValueError(f'Attempt to deserialize a non-set: {o.__class__}') +class TitleMapper(BaseHelper): + + @classmethod + def json_serialize(cls, o: str) -> str: + return f'{{J}} {o}' + + @classmethod + def json_deserialize(cls, o: str) -> str: + return re.sub(r'^\{J} ', '', o) + + @classmethod + def xml_serialize(cls, o: str) -> str: + return f'{{X}} {o}' + + @classmethod + def xml_deserialize(cls, o: str) -> str: + return re.sub(r'^\{X} ', '', o) + + @serializable.serializable_class class Chapter: @@ -223,6 +243,7 @@ def id(self) -> UUID: @property # type: ignore[misc] @serializable.xml_sequence(2) + @serializable.type_mapping(TitleMapper) def title(self) -> str: return self._title