diff options
Diffstat (limited to 'keystone/common')
-rw-r--r-- | keystone/common/serializer.py | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/keystone/common/serializer.py b/keystone/common/serializer.py new file mode 100644 index 00000000..c5e9b770 --- /dev/null +++ b/keystone/common/serializer.py @@ -0,0 +1,197 @@ +""" +Dict <--> XML de/serializer. + +The identity API prefers attributes over elements, so we serialize that way +by convention, with a few hardcoded exceptions. + +""" + +from lxml import etree +import re + + +DOCTYPE = '<?xml version="1.0" encoding="UTF-8"?>' +XMLNS = 'http://docs.openstack.org/identity/api/v2.0' + + +def from_xml(xml): + """Deserialize XML to a dictionary.""" + if xml is None: + return None + + deserializer = XmlDeserializer() + return deserializer(xml) + + +def to_xml(d, xmlns=None): + """Serialize a dictionary to XML.""" + if d is None: + return None + + serialize = XmlSerializer() + return serialize(d, xmlns) + + +class XmlDeserializer(object): + def __call__(self, xml_str): + """Returns a dictionary populated by decoding the given xml string.""" + dom = etree.fromstring(xml_str.strip()) + return self.walk_element(dom) + + @staticmethod + def _tag_name(tag): + """Remove the namespace from the tagname. + + TODO(dolph): We might care about the namespace at some point. + + >>> XmlDeserializer._tag_name('{xmlNamespace}tagName') + 'tagName' + + """ + m = re.search('[^}]+$', tag) + return m.string[m.start():] + + def walk_element(self, element): + """Populates a dictionary by walking an etree element.""" + values = {} + for k, v in element.attrib.iteritems(): + # boolean-looking attributes become booleans in JSON + if k in ['enabled']: + if v in ['true']: + v = True + elif v in ['false']: + v = False + + values[k] = v + + text = None + if element.text is not None: + text = element.text.strip() + + # current spec does not have attributes on an element with text + values = values or text or {} + + for child in [self.walk_element(x) for x in element]: + values = dict(values.items() + child.items()) + + return {XmlDeserializer._tag_name(element.tag): values} + + +class XmlSerializer(object): + def __call__(self, d, xmlns=None): + """Returns an xml etree populated by the given dictionary. + + Optionally, namespace the etree by specifying an ``xmlns``. + + """ + # FIXME(dolph): skipping links for now + for key in d.keys(): + if '_links' in key: + d.pop(key) + + assert len(d.keys()) == 1, ('Cannot encode more than one root ' + 'element: %s' % d.keys()) + + # name the root dom element + name = d.keys()[0] + + # only the root dom element gets an xlmns + root = etree.Element(name, xmlns=(xmlns or XMLNS)) + + self.populate_element(root, d[name]) + + # TODO(dolph): you can get a doctype from lxml, using ElementTrees + return '%s\n%s' % (DOCTYPE, etree.tostring(root, pretty_print=True)) + + def _populate_list(self, element, k, v): + """Populates an element with a key & list value.""" + # spec has a lot of inconsistency here! + container = element + + if k == 'media-types': + # xsd compliance: <media-types> contains <media-type>s + # find an existing <media-types> element or make one + container = element.find('media-types') + if container is None: + container = etree.Element(k) + element.append(container) + name = k[:-1] + elif k == 'serviceCatalog': + # xsd compliance: <serviceCatalog> contains <service>s + container = etree.Element(k) + element.append(container) + name = 'service' + elif k == 'values' and element.tag[-1] == 's': + # OS convention is to contain lists in a 'values' element, + # so the list itself can have attributes, which is + # unnecessary in XML + name = element.tag[:-1] + elif k[-1] == 's': + name = k[:-1] + else: + name = k + + for item in v: + child = etree.Element(name) + self.populate_element(child, item) + container.append(child) + + def _populate_dict(self, element, k, v): + """Populates an element with a key & dictionary value.""" + child = etree.Element(k) + self.populate_element(child, v) + element.append(child) + + def _populate_bool(self, element, k, v): + """Populates an element with a key & boolean value.""" + # booleans are 'true' and 'false' + element.set(k, unicode(v).lower()) + + def _populate_str(self, element, k, v): + """Populates an element with a key & string value.""" + if k in ['description']: + # always becomes an element + child = etree.Element(k) + child.text = unicode(v) + element.append(child) + else: + # add attributes to the current element + element.set(k, unicode(v)) + + def _populate_number(self, element, k, v): + """Populates an element with a key & numeric value.""" + # numbers can be handled as strings + self._populate_str(element, k, v) + + def populate_element(self, element, value): + """Populates an etree with the given value.""" + if isinstance(value, list): + self._populate_sequence(element, value) + elif isinstance(value, dict): + self._populate_tree(element, value) + + def _populate_sequence(self, element, l): + """Populates an etree with a sequence of elements, given a list.""" + # xsd compliance: child elements are singular: <users> has <user>s + name = element.tag + if element.tag[-1] == 's': + name = element.tag[:-1] + + for item in l: + child = etree.Element(name) + self.populate_element(child, item) + element.append(child) + + def _populate_tree(self, element, d): + """Populates an etree with attributes & elements, given a dict.""" + for k, v in d.iteritems(): + if isinstance(v, dict): + self._populate_dict(element, k, v) + elif isinstance(v, list): + self._populate_list(element, k, v) + elif isinstance(v, bool): + self._populate_bool(element, k, v) + elif isinstance(v, basestring): + self._populate_str(element, k, v) + elif type(v) in [int, float, long, complex]: + self._populate_number(element, k, v) |