1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
|
import sys
from collections import defaultdict
from typing import Any, List, Optional
from xsdata.codegen.models import Attr, AttrType, Class
from xsdata.codegen.utils import ClassUtils
from xsdata.formats.converter import converter
from xsdata.formats.dataclass.models.generics import AnyElement
from xsdata.models.enums import DataType, QNames, Tag
from xsdata.utils import collections
from xsdata.utils.namespaces import build_qname, split_qname
class ElementMapper:
"""Map a schema instance to classes, extensions and attributes."""
@classmethod
def map(cls, element: AnyElement, location: str) -> List[Class]:
"""Map schema children elements to classes."""
assert element.qname is not None
uri, name = split_qname(element.qname)
target = cls.build_class(element, uri)
return list(ClassUtils.flatten(target, f"{location}/{name}"))
@classmethod
def build_class(cls, element: AnyElement, parent_namespace: Optional[str]) -> Class:
assert element.qname is not None
namespace, name = split_qname(element.qname)
namespace = cls.select_namespace(namespace, parent_namespace)
target = Class(
qname=build_qname(namespace, name),
namespace=namespace,
tag=Tag.ELEMENT,
location="",
)
cls.build_attributes(target, element, namespace)
cls.build_elements(target, element, namespace)
cls.build_text(target, element)
return target
@classmethod
def build_attributes(
cls, target: Class, element: AnyElement, namespace: Optional[str]
):
for key, value in element.attributes.items():
if key == QNames.XSI_NIL:
target.nillable = value.strip() in ("true", "1")
else:
attr_type = cls.build_attribute_type(key, value)
cls.build_attribute(target, key, attr_type, namespace, Tag.ATTRIBUTE)
@classmethod
def build_elements(
cls, target: Class, element: AnyElement, namespace: Optional[str]
):
sequences = cls.sequential_groups(element)
for index, child in enumerate(element.children):
if isinstance(child, AnyElement) and child.qname:
if child.tail:
target.mixed = True
if child.attributes or child.children:
inner = cls.build_class(child, namespace)
attr_type = AttrType(qname=inner.qname, forward=True)
target.inner.append(inner)
else:
attr_type = cls.build_attribute_type(child.qname, child.text)
sequence = collections.find_connected_component(sequences, index)
cls.build_attribute(
target,
child.qname,
attr_type,
namespace,
Tag.ELEMENT,
sequence + 1,
)
@classmethod
def build_text(cls, target: Class, element: AnyElement):
if element.text:
attr_type = cls.build_attribute_type("value", element.text)
cls.build_attribute(target, "value", attr_type, None, Tag.SIMPLE_TYPE)
if any(attr.tag == Tag.ELEMENT for attr in target.attrs):
target.mixed = True
@classmethod
def build_attribute_type(cls, qname: str, value: Any) -> AttrType:
def match_type(val: Any) -> DataType:
if not isinstance(val, str):
return DataType.from_value(val)
for tp in converter.explicit_types():
if converter.test(val, [tp], strict=True):
return DataType.from_type(tp)
return DataType.STRING
if qname == QNames.XSI_TYPE:
data_type = DataType.QNAME
elif value is None or value == "":
data_type = DataType.ANY_SIMPLE_TYPE
else:
data_type = match_type(value)
return AttrType(qname=str(data_type), native=True)
@classmethod
def build_attribute(
cls,
target: Class,
qname: str,
attr_type: AttrType,
parent_namespace: Optional[str] = None,
tag: str = Tag.ELEMENT,
sequence: int = 0,
):
namespace, name = split_qname(qname)
namespace = cls.select_namespace(namespace, parent_namespace, tag)
index = len(target.attrs)
attr = Attr(index=index, name=name, tag=tag, namespace=namespace)
attr.types.append(attr_type)
if sequence:
attr.restrictions.path.append(("s", sequence, 1, sys.maxsize))
attr.restrictions.min_occurs = 1
attr.restrictions.max_occurs = 1
cls.add_attribute(target, attr)
@classmethod
def add_attribute(cls, target: Class, attr: Attr):
pos = collections.find(target.attrs, attr)
if pos > -1:
existing = target.attrs[pos]
existing.restrictions.max_occurs = sys.maxsize
existing.types.extend(attr.types)
existing.types = collections.unique_sequence(existing.types, key="qname")
else:
target.attrs.append(attr)
@classmethod
def select_namespace(
cls,
namespace: Optional[str],
parent_namespace: Optional[str],
tag: str = Tag.ELEMENT,
) -> Optional[str]:
if tag == Tag.ATTRIBUTE:
return namespace
if namespace is None and parent_namespace is not None:
return ""
return namespace
@classmethod
def sequential_groups(cls, element: AnyElement) -> List[List[int]]:
groups = cls.group_repeating_attrs(element)
return list(collections.connected_components(groups))
@classmethod
def group_repeating_attrs(cls, element: AnyElement) -> List[List[int]]:
counters = defaultdict(list)
for index, child in enumerate(element.children):
if isinstance(child, AnyElement) and child.qname:
counters[child.qname].append(index)
groups = []
if len(counters) > 1:
for x in counters.values():
if len(x) > 1:
groups.append(list(range(x[0], x[-1] + 1)))
return groups
|