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 184 185 186 187 188 189 190 191 192 193 194 195 196
|
from xsdata.codegen.mixins import RelativeHandlerInterface
from xsdata.codegen.models import Attr, AttrType, Class
from xsdata.formats.converter import converter
from xsdata.logger import logger
from xsdata.models.enums import DataType
class SanitizeAttributesDefaultValue(RelativeHandlerInterface):
"""
Sanitize attributes default values.
Cases:
1. Ignore enumerations.
2. List fields can not have a default value
3. Optional choice/sequence fields can not have a default value
4. xsi:type fields are ignored, mark them as optional
5. Convert string literal default value for enum fields.
"""
__slots__ = ()
def process(self, target: Class):
for attr in target.attrs:
self.process_attribute(target, attr)
for choice in attr.choices:
self.process_attribute(target, choice)
def process_attribute(self, target: Class, attr: Attr):
if self.should_reset_required(attr):
attr.restrictions.min_occurs = 0
if self.should_reset_default(attr):
attr.fixed = False
attr.default = None
if attr.default is not None:
self.process_types(target, attr)
elif attr.xml_type is None and str in attr.native_types:
# String text nodes get an empty string as default!
attr.default = ""
def process_types(self, target: Class, attr: Attr):
if self.is_valid_external_value(target, attr):
return
if self.is_valid_native_value(target, attr):
return
logger.warning(
"Failed to match %s.%s default value `%s` to one of %s",
target.name,
attr.local_name,
attr.default,
[tp.qname for tp in attr.types],
)
self.reset_attribute_types(attr)
def is_valid_external_value(self, target: Class, attr: Attr) -> bool:
"""Return whether the default value of the given attr can be mapped to
user defined type like an enumeration or an inner complex content
class."""
for tp in attr.user_types:
source = self.find_type(target, tp)
if self.is_valid_inner_type(source, attr, tp):
return True
if self.is_valid_enum_type(source, attr):
return True
return False
def find_type(self, target: Class, attr_type: AttrType) -> Class:
if attr_type.forward:
return self.container.find_inner(target, attr_type.qname)
return self.container.first(attr_type.qname)
@classmethod
def is_valid_inner_type(
cls, source: Class, attr: Attr, attr_type: AttrType
) -> bool:
"""Return whether the inner class can inherit the attr default value
and swap them as well."""
if attr_type.forward:
for src_attr in source.attrs:
if src_attr.xml_type is None:
src_attr.default = attr.default
src_attr.fixed = attr.fixed
attr.default = None
attr.fixed = False
return True
return False
@classmethod
def is_valid_enum_type(cls, source: Class, attr: Attr) -> bool:
"""
Convert string literal default values to enumeration members
placeholders and return result.
The placeholders will be converted to proper references from the
generator filters.
Placeholder examples: Single -> @enum@qname::member_name
Multiple -> @enum@qname::first_member@second_member
"""
assert attr.default is not None
value_members = {x.default: x.name for x in source.attrs}
name = value_members.get(attr.default)
if name:
attr.default = f"@enum@{source.qname}::{name}"
return True
names = [
value_members[token]
for token in attr.default.split()
if token in value_members
]
if names:
attr.default = f"@enum@{source.qname}::{'@'.join(names)}"
return True
return False
@classmethod
def is_valid_native_value(cls, target: Class, attr: Attr) -> bool:
"""
Return whether the default value of the given attribute can be
converted successfully to and from xml.
The test process for enumerations and fixed value fields are
strict, meaning the textual representation also needs to match
the original.
"""
assert attr.default is not None
types = converter.sort_types(attr.native_types)
if not types:
return False
tokens = attr.default.split() if attr.restrictions.tokens else [attr.default]
if len(tokens) == 1 and attr.is_enumeration and attr.restrictions.tokens:
attr.restrictions.tokens = False
# Enumerations are also fixed!!!
strict = attr.fixed
return all(
converter.test(
token,
types,
strict=strict,
ns_map=target.ns_map,
format=attr.restrictions.format,
)
for token in tokens
)
@classmethod
def should_reset_required(cls, attr: Attr) -> bool:
"""
Return whether the min occurrences for the attr needs to be reset.
@Todo figure out if wildcards are supposed to be optional!
"""
return (
not attr.is_attribute
and attr.default is None
and object in attr.native_types
and not attr.is_list
)
@classmethod
def should_reset_default(cls, attr: Attr) -> bool:
"""
Return whether we should unset the default value of the attribute.
- Default value is not set
- Attribute is xsi:type (ignorable)
- Attribute is part of a choice
"""
return attr.default is not None and (
attr.is_xsi_type
or attr.is_list
or (not attr.is_attribute and attr.is_optional)
)
@classmethod
def reset_attribute_types(cls, attr: Attr):
attr.types.clear()
attr.types.append(AttrType(qname=str(DataType.STRING), native=True))
attr.restrictions.format = None
|