Skip to content

Commit

Permalink
feat: add support for defining XML element ordering with `@serializab…
Browse files Browse the repository at this point in the history
…le.xml_sequence()` decorator

Signed-off-by: Paul Horton <paul.horton@owasp.org>
  • Loading branch information
madpah committed Sep 5, 2022
1 parent 3bbfb1b commit c1442ae
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 60 deletions.
162 changes: 102 additions & 60 deletions serializable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,10 @@ def _as_xml(self: _T, as_string: bool = True, element_name: Optional[str] = None

this_e_attributes = {}
klass_qualified_name = f'{self.__module__}.{self.__class__.__qualname__}'
serializable_property_info = ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {})
serializable_property_info = {k: v for k, v in sorted(
ObjectMetadataLibrary.klass_property_mappings.get(klass_qualified_name, {}).items(),
key=lambda i: i[1].xml_sequence)}

# Handle any Properties that should be attributes
for k, v in self.__dict__.items():
# Remove leading _ in key names
new_key = k[1:]
Expand Down Expand Up @@ -265,73 +266,69 @@ def _as_xml(self: _T, as_string: bool = True, element_name: Optional[str] = None
this_e = ElementTree.Element(element_name, this_e_attributes)

# Handle remaining Properties that will be sub elements
for k, v in self.__dict__.items():
for k, prop_info in serializable_property_info.items():
v = getattr(self, k)

# Ignore None values by default
if v is None:
continue

# Remove leading _ in key names
new_key = k[1:]
if new_key.startswith('_') or '__' in new_key:
continue
new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=new_key)
new_key = BaseNameFormatter.decode_handle_python_builtins_and_keywords(name=k)

if new_key in serializable_property_info:
prop_info = serializable_property_info.get(new_key)
if not prop_info:
raise ValueError(f'{new_key} is not a known Property for {klass_qualified_name}')
if not prop_info:
raise ValueError(f'{new_key} is not a known Property for {klass_qualified_name}')

if not prop_info.is_xml_attribute:
new_key = prop_info.custom_names.get(SerializationType.XML, new_key)
if not prop_info.is_xml_attribute:
new_key = prop_info.custom_names.get(SerializationType.XML, new_key)

if new_key == '.':
this_e.text = str(v)
continue
if new_key == '.':
this_e.text = str(v)
continue

if CurrentFormatter.formatter:
new_key = CurrentFormatter.formatter.encode(property_name=new_key)
new_key = _namespace_element_name(tag_name=new_key, xmlns=xmlns)
if CurrentFormatter.formatter:
new_key = CurrentFormatter.formatter.encode(property_name=new_key)
new_key = _namespace_element_name(tag_name=new_key, xmlns=xmlns)

if prop_info.custom_type:
if prop_info.is_helper_type():
ElementTree.SubElement(this_e, new_key).text = str(prop_info.custom_type.serialize(v))
else:
ElementTree.SubElement(this_e, new_key).text = str(prop_info.custom_type(v))
elif prop_info.is_array and prop_info.xml_array_config:
_array_type, nested_key = prop_info.xml_array_config
nested_key = _namespace_element_name(tag_name=nested_key, xmlns=xmlns)
if _array_type and _array_type == XmlArraySerializationType.NESTED and len(v) > 0:
nested_e = ElementTree.SubElement(this_e, new_key)
else:
nested_e = this_e

for j in v:
if not prop_info.is_primitive_type():
nested_e.append(j.as_xml(as_string=False, element_name=nested_key, xmlns=xmlns))
elif prop_info.concrete_type() in (float, int):
ElementTree.SubElement(nested_e, nested_key).text = str(j)
elif prop_info.concrete_type() is bool:
ElementTree.SubElement(nested_e, nested_key).text = str(j).lower()
else:
# Assume type is str
ElementTree.SubElement(nested_e, nested_key).text = str(j)
elif prop_info.is_enum:
ElementTree.SubElement(this_e, new_key).text = str(v.value)
elif not prop_info.is_primitive_type():
global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}'
if global_klass_name in ObjectMetadataLibrary.klass_mappings:
# Handle other Serializable Classes
this_e.append(v.as_xml(as_string=False, element_name=new_key, xmlns=xmlns))
if prop_info.custom_type:
if prop_info.is_helper_type():
ElementTree.SubElement(this_e, new_key).text = str(prop_info.custom_type.serialize(v))
else:
ElementTree.SubElement(this_e, new_key).text = str(prop_info.custom_type(v))
elif prop_info.is_array and prop_info.xml_array_config:
_array_type, nested_key = prop_info.xml_array_config
nested_key = _namespace_element_name(tag_name=nested_key, xmlns=xmlns)
if _array_type and _array_type == XmlArraySerializationType.NESTED and len(v) > 0:
nested_e = ElementTree.SubElement(this_e, new_key)
else:
nested_e = this_e

for j in v:
if not prop_info.is_primitive_type():
nested_e.append(j.as_xml(as_string=False, element_name=nested_key, xmlns=xmlns))
elif prop_info.concrete_type in (float, int):
ElementTree.SubElement(nested_e, nested_key).text = str(j)
elif prop_info.concrete_type is bool:
ElementTree.SubElement(nested_e, nested_key).text = str(j).lower()
else:
# Handle properties that have a type that is not a Python Primitive (e.g. int, float, str)
ElementTree.SubElement(this_e, new_key).text = str(v)
elif prop_info.type_ in (float, int):
ElementTree.SubElement(this_e, new_key).text = str(v)
elif prop_info.type_ is bool:
ElementTree.SubElement(this_e, new_key).text = str(v).lower()
# Assume type is str
ElementTree.SubElement(nested_e, nested_key).text = str(j)
elif prop_info.is_enum:
ElementTree.SubElement(this_e, new_key).text = str(v.value)
elif not prop_info.is_primitive_type():
global_klass_name = f'{prop_info.concrete_type.__module__}.{prop_info.concrete_type.__name__}'
if global_klass_name in ObjectMetadataLibrary.klass_mappings:
# Handle other Serializable Classes
this_e.append(v.as_xml(as_string=False, element_name=new_key, xmlns=xmlns))
else:
# Assume type is str
# Handle properties that have a type that is not a Python Primitive (e.g. int, float, str)
ElementTree.SubElement(this_e, new_key).text = str(v)
elif prop_info.concrete_type in (float, int):
ElementTree.SubElement(this_e, new_key).text = str(v)
elif prop_info.concrete_type is bool:
ElementTree.SubElement(this_e, new_key).text = str(v).lower()
else:
# Assume type is str
ElementTree.SubElement(this_e, new_key).text = str(v)

if as_string:
return ElementTree.tostring(this_e, 'unicode')
Expand Down Expand Up @@ -485,6 +482,7 @@ class ObjectMetadataLibrary:
_klass_property_attributes: Set[str] = set()
_klass_property_names: Dict[str, Dict[SerializationType, str]] = {}
_klass_property_types: Dict[str, Type[Any]] = {}
_klass_property_xml_sequence: Dict[str, int] = {}
klass_mappings: Dict[str, 'ObjectMetadataLibrary.SerializableClass'] = {}
klass_property_mappings: Dict[str, Dict[str, 'ObjectMetadataLibrary.SerializableProperty']] = {}

Expand Down Expand Up @@ -540,7 +538,8 @@ class SerializableProperty:

def __init__(self, *, prop_name: str, prop_type: Any, custom_names: Dict[SerializationType, str],
custom_type: Optional[Any] = None, is_xml_attribute: bool = False,
xml_array_config: Optional[Tuple[XmlArraySerializationType, str]] = None) -> None:
xml_array_config: Optional[Tuple[XmlArraySerializationType, str]] = None,
xml_sequence: Optional[int] = None) -> None:
self._name = prop_name
self._custom_names = custom_names
self._type_ = None
Expand All @@ -551,6 +550,7 @@ def __init__(self, *, prop_name: str, prop_type: Any, custom_names: Dict[Seriali
self._custom_type = custom_type
self._is_xml_attribute = is_xml_attribute
self._xml_array_config = xml_array_config
self._xml_sequence = xml_sequence or 100

self._deferred_type_parsing = False
self._parse_type(type_=prop_type)
Expand Down Expand Up @@ -598,6 +598,10 @@ def is_enum(self) -> bool:
def is_optional(self) -> bool:
return self._is_optional

@property
def xml_sequence(self) -> int:
return self._xml_sequence

def is_helper_type(self) -> bool:
if inspect.isclass(self.custom_type):
return issubclass(self.custom_type, BaseHelper)
Expand Down Expand Up @@ -678,11 +682,28 @@ def _parse_type(self, type_: Any) -> None:
if self._deferred_type_parsing:
self._deferred_type_parsing = False

def __eq__(self, other) -> bool:
if isinstance(other, ObjectMetadataLibrary.SerializableProperty):
return hash(other) == hash(self)
return False

def __lt__(self, other) -> bool:
if isinstance(other, ObjectMetadataLibrary.SerializableProperty):
return self.xml_sequence < other.xml_sequence
return NotImplemented

def __hash__(self) -> int:
return hash((
self.concrete_type, tuple(self.custom_names), self.custom_type, self.is_array, self.is_enum,
self.is_optional, self.is_xml_attribute, self.name, self.type_, tuple(self.xml_array_config),
self.xml_sequence
))

def __repr__(self) -> str:
return f'<s.oml.SerializableProperty name={self.name}, custom_names={self.custom_names}, ' \
f'array={self.is_array}, enum={self.is_enum}, optional={self.is_optional}, ' \
f'c_type={self.concrete_type}, type={self.type_}, custom_type={self.custom_type}, ' \
f'xml_attr={self.is_xml_attribute}>'
f'xml_attr={self.is_xml_attribute}, xml_sequence={self.xml_sequence}>'

@classmethod
def defer_property_type_parsing(cls, prop: 'ObjectMetadataLibrary.SerializableProperty',
Expand Down Expand Up @@ -732,7 +753,8 @@ def register_klass(cls, klass: _T, custom_name: Optional[str],
is_xml_attribute=(qualified_property_name in ObjectMetadataLibrary._klass_property_attributes),
xml_array_config=ObjectMetadataLibrary._klass_property_array_config.get(
qualified_property_name, None
)
),
xml_sequence=ObjectMetadataLibrary._klass_property_xml_sequence.get(qualified_property_name, 100)
)
})

Expand Down Expand Up @@ -774,6 +796,10 @@ def register_xml_property_array_config(cls, qual_name: str,
def register_xml_property_attribute(cls, qual_name: str) -> None:
cls._klass_property_attributes.add(qual_name)

@classmethod
def register_xml_property_sequence(cls, qual_name: str, sequence: int) -> None:
cls._klass_property_xml_sequence.update({qual_name: sequence})

@classmethod
def register_property_type_mapping(cls, qual_name: str, mapped_type: Any) -> None:
cls._klass_property_types.update({qual_name: mapped_type})
Expand Down Expand Up @@ -893,3 +919,19 @@ def inner(*args: Any, **kwargs: Any) -> Any:
return cast(_F, inner)

return outer


def xml_sequence(sequence: int) -> Callable[[_F], _F]:
def outer(f: _F) -> _F:
logger.debug(f'Registering {f.__module__}.{f.__qualname__} with XML sequence: {sequence}')
ObjectMetadataLibrary.register_xml_property_sequence(
qual_name=f'{f.__module__}.{f.__qualname__}', sequence=sequence
)

@functools.wraps(f)
def inner(*args: Any, **kwargs: Any) -> Any:
return f(*args, **kwargs)

return cast(_F, inner)

return outer
2 changes: 2 additions & 0 deletions tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[
self._type_ = type_

@property
@serializable.xml_sequence(1)
def id_(self) -> UUID:
return self._id_

Expand Down Expand Up @@ -172,6 +173,7 @@ def chapters(self, chapters: Iterable[Chapter]) -> None:
self._chapters = list(chapters)

@property
@serializable.xml_sequence(2)
def type_(self) -> BookType:
return self._type_

Expand Down

0 comments on commit c1442ae

Please sign in to comment.