Skip to content

Commit

Permalink
fix: fully handle NULL and empty lists
Browse files Browse the repository at this point in the history
  • Loading branch information
nigelm committed Jun 28, 2022
1 parent 4701ec5 commit 2f5f590
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 26 deletions.
80 changes: 55 additions & 25 deletions broadworks_ocip/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@

logger = logging.getLogger(__name__)

# constants
XML_NIL = "{http://www.w3.org/2001/XMLSchema-instance}nil"
XSD_INST = "http://www.w3.org/2001/XMLSchema-instance"
XSD_TYPE = "{http://www.w3.org/2001/XMLSchema-instance}type"


@attr.s(slots=True, frozen=True)
class ElementInfo:
Expand Down Expand Up @@ -92,7 +97,7 @@ def __init__(self, **kwargs):
f"{cname}: Expected {elem.name} to be a table/list but it is {type(value)}",
)
elif elem.is_container:
if not isinstance(value, dict):
if value is not None and not isinstance(value, dict):
raise TypeError(
f"{cname}: Expected {elem.name} to be a dict of elements but it is {type(value)}",
)
Expand Down Expand Up @@ -193,8 +198,11 @@ def etree_sub_components_(self, element: etree._Element) -> etree._Element:
value = getattr(self, sub_element.name)
if sub_element.is_array:
if value is not None:
for subvalue in value:
self.etree_sub_element_(element, sub_element, subvalue)
if len(value) == 0:
element.attrib[XML_NIL] = "true"
else:
for subvalue in value:
self.etree_sub_element_(element, sub_element, subvalue)
else:
self.etree_sub_element_(element, sub_element, value)
return element
Expand All @@ -221,7 +229,7 @@ def etree_sub_element_(
etree.SubElement(
element,
sub_element.xmlname,
{"{http://www.w3.org/2001/XMLSchema-instance}nil": "true"},
{XML_NIL: "true"},
nsmap=self._default_nsmap(),
)
elif value is None:
Expand Down Expand Up @@ -257,7 +265,7 @@ def etree_sub_element_(
elem = etree.SubElement(
element,
sub_element.xmlname,
{"{http://www.w3.org/2001/XMLSchema-instance}type": value.type_},
{XSD_TYPE: value.type_},
nsmap=self._default_nsmap(),
)
else:
Expand Down Expand Up @@ -365,20 +373,26 @@ def build_from_node_(
results: Object instance for this class
"""
if node is not None:
if elem.is_table:
if XML_NIL in node.attrib and node.attrib[XML_NIL] == "true":
return Null
elif elem.is_table:
return cls.decode_table_(node)
elif elem.is_complex:
if elem.is_abstract:
if "{http://www.w3.org/2001/XMLSchema-instance}type" in node.attrib:
if XSD_TYPE in node.attrib:
thisclass = api.get_type_class(
node.attrib[
"{http://www.w3.org/2001/XMLSchema-instance}type"
],
node.attrib[XSD_TYPE],
)
logger.error(f"thisclass={thisclass}")
return thisclass.build_from_etree_(api=api, element=node)
else:
return ValueError("Cannot decode abstract object")
elif elem.is_container:
return cls.build_initialiser_from_etree_(
api=api,
element=node,
subelement_set=elem.type,
)
else:
return elem.type.build_from_etree_(api=api, element=node)
elif elem.type == bool:
Expand All @@ -391,23 +405,15 @@ def build_from_node_(
return None

@classmethod
def build_from_etree_(
def build_initialiser_from_etree_(
cls,
api: "BroadworksAPI", # type: ignore # noqa
element: etree._Element,
subelement_set: List[ElementInfo],
extras: Dict[str, Any] = {},
):
"""
Create an OciType based instance from an XML etree element
Arguments:
element: The OCIType XML element
Returns:
results: Object instance for this class
"""
initialiser = extras.copy()
for elem in cls._elements():
for elem in subelement_set:
if elem.is_array:
result = []
nodes = element.findall(elem.xmlname)
Expand All @@ -426,6 +432,30 @@ def build_from_etree_(
# I am inclined to thow an error here - at least after checking if
# the thing is require, but the class builder should do that so lets
# let it do its thing
return initialiser

@classmethod
def build_from_etree_(
cls,
api: "BroadworksAPI", # type: ignore # noqa
element: etree._Element,
extras: Dict[str, Any] = {},
):
"""
Create an OciType based instance from an XML etree element
Arguments:
element: The OCIType XML element
Returns:
results: Object instance for this class
"""
initialiser = cls.build_initialiser_from_etree_(
api=api,
element=element,
subelement_set=list(cls._elements()),
extras=extras,
)
# now have a dict with all the bits in it.
# use that to build a new object
return cls(**initialiser)
Expand All @@ -446,7 +476,7 @@ def to_dict(self) -> Dict[str, Any]:
elif elem.is_complex:
if elem.is_array:
value = [x.to_dict() for x in value]
else:
elif value is not None:
value = value.to_dict()
elif elem.is_array:
value = [x for x in value]
Expand Down Expand Up @@ -530,7 +560,7 @@ def build_xml_command_element_(self, root: etree._Element) -> etree._Element:
return etree.SubElement(
root,
"command",
{"{http://www.w3.org/2001/XMLSchema-instance}type": self.type_},
{XSD_TYPE: self.type_},
nsmap=self._default_nsmap(),
)

Expand Down Expand Up @@ -585,7 +615,7 @@ def build_xml_command_element_(self, root: etree._Element) -> etree._Element:
return etree.SubElement(
root,
"command",
{"echo": "", "{http://www.w3.org/2001/XMLSchema-instance}type": self.type_},
{"echo": "", XSD_TYPE: self.type_},
nsmap=self._default_nsmap(),
)

Expand Down Expand Up @@ -652,7 +682,7 @@ def build_xml_command_element_(self, root):
{
"type": "Error",
"echo": "",
"{http://www.w3.org/2001/XMLSchema-instance}type": "c:" + self.type_,
XSD_TYPE: "c:" + self.type_,
},
nsmap=self._error_nsmap(),
)
Expand Down
41 changes: 40 additions & 1 deletion tests/test_basic_command_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,6 @@ def test_nested_elements():
user_id="user@example.com",
phone_number="123456789",
extension="1219",
# sip_alias_list=api.get_type_object("ReplacementSIPAliasList",sip_alias=[]),
sip_alias_list=Null,
endpoint=dict(
trunk_addressing=api.get_type_object(
Expand Down Expand Up @@ -462,4 +461,44 @@ def test_nested_elements():
)


def test_nested_elements2():
cmd = api.get_command_object(
"UserModifyRequest22",
user_id="user@example.com",
phone_number="123456789",
extension="1219",
sip_alias_list=api.get_type_object("ReplacementSIPAliasList", sip_alias=[]),
endpoint=dict(
trunk_addressing=api.get_type_object(
"TrunkAddressingMultipleContactModify",
trunk_group_device_endpoint=Null,
enterprise_trunk_name="ET02",
alternate_trunk_identity=Null,
),
),
)
check_command_xml(
(
b'<?xml version="1.0" encoding="ISO-8859-1"?>'
b'<BroadsoftDocument protocol="OCI" xmlns="C" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
b'<sessionId xmlns="">00000000-1111-2222-3333-444444444444</sessionId>'
b'<command xsi:type="UserModifyRequest22" xmlns="">'
b"<userId>user@example.com</userId>"
b"<phoneNumber>123456789</phoneNumber>"
b"<extension>1219</extension>"
b'<sipAliasList xsi:nil="true"/>'
b"<endpoint>"
b"<trunkAddressing>"
b'<trunkGroupDeviceEndpoint xsi:nil="true"/>'
b"<enterpriseTrunkName>ET02</enterpriseTrunkName>"
b'<alternateTrunkIdentity xsi:nil="true"/>'
b"</trunkAddressing>"
b"</endpoint>"
b"</command>"
b"</BroadsoftDocument>"
),
cmd,
)


# end
42 changes: 42 additions & 0 deletions tests/test_xml_command_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import namedtuple

import pytest # noqa: F401
from null_object import Null

from broadworks_ocip import BroadworksAPI

Expand Down Expand Up @@ -319,4 +320,45 @@ def test_group_department_add_xml():
)


def test_nested_elements():
xml = (
b'<?xml version="1.0" encoding="ISO-8859-1"?>'
b'<BroadsoftDocument protocol="OCI" xmlns="C" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
b'<sessionId xmlns="">00000000-1111-2222-3333-444444444444</sessionId>'
b'<command xsi:type="UserModifyRequest22" xmlns="">'
b"<userId>user@example.com</userId>"
b"<phoneNumber>123456789</phoneNumber>"
b"<extension>1219</extension>"
b'<sipAliasList xsi:nil="true"/>'
b"<endpoint>"
b"<trunkAddressing>"
b'<trunkGroupDeviceEndpoint xsi:nil="true"/>'
b"<enterpriseTrunkName>ET02</enterpriseTrunkName>"
b'<alternateTrunkIdentity xsi:nil="true"/>'
b"</trunkAddressing>"
b"</endpoint>"
b"</command>"
b"</BroadsoftDocument>"
)
api = BroadworksAPI(**BASIC_API_PARAMS)
generated = api.decode_xml(xml)
assert generated.type_ == "UserModifyRequest22"
assert generated.user_id == "user@example.com"
assert generated.phone_number == "123456789"
assert generated.sip_alias_list is Null

# assert generated.sip_alias_list is Null
print(generated.endpoint["trunk_addressing"])
assert (
generated.endpoint["trunk_addressing"].to_dict()
== api.get_type_object( # noqa: W503
"TrunkAddressingMultipleContactModify",
trunk_group_device_endpoint=Null,
enterprise_trunk_name="ET02",
alternate_trunk_identity=Null,
# physical_location=None,
).to_dict()
)


# end

0 comments on commit 2f5f590

Please sign in to comment.