Refactor RNG export for later support of earlier SEDA version
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 07 Oct 2016 17:48:55 +0200
changeset 1624 3b80079a761d
parent 1623 23f825b308db
child 1625 f75c007abed2
Refactor RNG export for later support of earlier SEDA version At some time, more code cleanup / reorganisation will be necessary, but this preparatory refactoring will allow to share usage of cardinality handling function as well as a basic mixin for RNG generation. Related to #15045341
entities/profile_generation.py
--- a/entities/profile_generation.py	Fri Oct 07 13:22:26 2016 +0200
+++ b/entities/profile_generation.py	Fri Oct 07 17:48:55 2016 +0200
@@ -108,25 +108,32 @@
     return text_type(value)
 
 
+def minmax_cardinality(string_cardinality, _allowed=('0..1', '0..n', '1', '1..n')):
+    """Return (minimum, maximum) cardinality for the cardinality as string (one of '0..1', '0..n',
+    '1' or '1..n').
+    """
+    assert string_cardinality in _allowed
+    if string_cardinality[0] == '0':
+        minimum = 0
+    else:
+        minimum = 1
+    if string_cardinality[-1] == 'n':
+        maximum = graph_nodes.INFINITY
+    else:
+        maximum = 1
+    return minimum, maximum
+
+
 def element_minmax_cardinality(occ, card_entity):
-    """Return (minimum, maximum) cardinality for the given pyxst Occurence. Cardinality may be
-    overriden by the data model's user_cardinality value.
+    """Return (minimum, maximum) cardinality for the given pyxst Occurence and entity.
+
+    Occurence 's cardinality may be overriden by the entity's user_cardinality value.
     """
     cardinality = getattr(card_entity, 'user_cardinality', None)
     if cardinality is None:
-        minimum = occ.minimum
-        maximum = occ.maximum
+        return occ.minimum, occ.maximum
     else:
-        assert cardinality in ('0..1', '0..n', '1', '1..n')
-        if cardinality[0] == '0':
-            minimum = 0
-        else:
-            minimum = 1
-        if cardinality[-1] == 'n':
-            maximum = graph_nodes.INFINITY
-        else:
-            maximum = 1
-    return minimum, maximum
+        return minmax_cardinality(cardinality)
 
 
 def attribute_minimum_cardinality(occ, card_entity):
@@ -135,14 +142,9 @@
     """
     cardinality = getattr(card_entity, 'user_cardinality', None)
     if cardinality is None:
-        minimum = occ.minimum
+        return occ.minimum
     else:
-        # XXX assert cardinality in ('0..1', '1'), cardinality
-        if cardinality[0] == '0':
-            minimum = 0
-        else:
-            minimum = 1
-    return minimum
+        return minmax_cardinality(cardinality, ('0..1', '1'))[0]
 
 
 def iter_path_children(xselement, entity):
@@ -162,6 +164,54 @@
                 yield _path, related
 
 
+class RNGMixin(object):
+    """Mixin class providing some Relax NG schema generation helper methods."""
+
+    def rng_element_parent(self, parent, minimum, maximum=1):
+        """Given a etree node and minimum/maximum cardinalities of a desired child element,
+        return suitable parent node for it.
+
+        This will be one of rng:optional, rng:zeroOrMore or rng:oneOrMore that will be created by
+        this method or the given parent itself if minimum == maximum == 1.
+        """
+        if minimum == 1 and maximum == 1:
+            return parent
+        elif minimum == 0 and maximum == 1:
+            return self.element('rng:optional', parent)
+        elif minimum == 0 and maximum == graph_nodes.INFINITY:
+            return self.element('rng:zeroOrMore', parent)
+        elif minimum == 1 and maximum == graph_nodes.INFINITY:
+            return self.element('rng:oneOrMore', parent)
+        else:
+            assert False, ('unexpected min/max cardinality:', minimum, maximum)
+
+    def rng_attribute_parent(self, parent, minimum):
+        """Given a etree node and minimum cardinality of a desired attribute,
+        return suitable parent node for it.
+
+        This will be rng:optional that will be created by this method or the given parent itself if
+        minimum == 1.
+        """
+        if minimum == 1:
+            return parent
+        else:
+            return self.element('rng:optional', parent)
+
+    def rng_value(self, element, qualified_datatype, fixed_value=None):
+        """Given a (etree) schema element, a data type (e.g. 'xsd:token') and an optional fixed
+        value, add RNG declaration to the element to declare the datatype and fix the value if
+        necessary.
+        """
+        prefix, datatype = qualified_datatype.split(':')
+        type_attrs = {'type': datatype}
+        if prefix != 'xsd':
+            type_attrs['datatypeLibrary'] = self.namespaces[prefix]
+        if fixed_value is not None:
+            self.element('rng:value', element, type_attrs, text=fixed_value)
+        else:
+            self.element('rng:data', element, type_attrs)
+
+
 class SEDA2ExportAdapter(EntityAdapter):
     """Abstract base class for export of SEDA profile."""
     __abstract__ = True
@@ -499,7 +549,7 @@
         return content_type
 
 
-class SEDA2RelaxNGExport(SEDA2ExportAdapter):
+class SEDA2RelaxNGExport(RNGMixin, SEDA2ExportAdapter):
     """Adapter to build a Relax NG representation of a SEDA profile, using SEDA 2.0 specification.
     """
     __regid__ = 'SEDA-2.0.rng'
@@ -656,22 +706,11 @@
 
     def _rng_element_parent(self, occ, card_entity, profile_element):
         minimum, maximum = element_minmax_cardinality(occ, card_entity)
-        if minimum == 1 and maximum == 1:
-            return profile_element
-        elif minimum == 0 and maximum == 1:
-            return self.element('rng:optional', profile_element)
-        elif minimum == 0 and maximum == graph_nodes.INFINITY:
-            return self.element('rng:zeroOrMore', profile_element)
-        elif minimum == 1 and maximum == graph_nodes.INFINITY:
-            return self.element('rng:oneOrMore', profile_element)
-        else:
-            assert False, ('unexpected min/max cardinality:', minimum, maximum)
+        return self.rng_element_parent(profile_element, minimum, maximum)
 
     def _rng_attribute_parent(self, occ, card_entity, profile_element):
-        if attribute_minimum_cardinality(occ, card_entity) == 1:
-            return profile_element
-        else:
-            return self.element('rng:optional', profile_element)
+        minimum = attribute_minimum_cardinality(occ, card_entity)
+        return self.rng_element_parent(profile_element, minimum)
 
     def _rng_attribute(self, xselement, parent_element, value=None):
         xstypes = content_types(xselement.textual_content_type)