soap: handle recursive complexType (#81643)

Reference to already converted complexType are converted to JSON schema
references.
This commit is contained in:
Benjamin Dauvergne 2023-09-26 17:04:34 +02:00
parent 117743e0a6
commit 8266740b52
2 changed files with 79 additions and 36 deletions

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import collections
import hashlib
import zeep
import zeep.helpers
@ -184,42 +185,61 @@ class SOAPConnector(BaseResource, HTTPResource):
operations_and_schemas.append((name, input_schema, output_schema))
return operations_and_schemas
def type2schema(self, xsd_type, keep_root=False, compress=False):
# simplify schema: when a type contains a unique element, it will try
# to match any dict or list with it on input and will flatten the
# schema on output.
if (
isinstance(xsd_type, zeep.xsd.ComplexType)
and len(xsd_type.elements) == 1
and not keep_root
and compress
):
if xsd_type.elements[0][1].max_occurs != 1:
@classmethod
def type2schema(cls, xsd_type, keep_root=False, compress=False):
seen = set()
def to_id(s):
return f'ref-{hashlib.md5(str(s).encode()).hexdigest()}'
def t2s(xsd_type):
type_name = xsd_type.qname or xsd_type.name
if isinstance(xsd_type, zeep.xsd.ComplexType):
if type_name in seen:
return {'$ref': '#' + to_id(type_name)}
seen.add(type_name)
# simplify schema: when a type contains a unique element, it will try
# to match any dict or list with it on input and will flatten the
# schema on output.
if (
isinstance(xsd_type, zeep.xsd.ComplexType)
and len(xsd_type.elements) == 1
and not keep_root
and compress
# and is not recursive
and xsd_type.elements[0][1].type != xsd_type
):
if xsd_type.elements[0][1].max_occurs != 1:
schema = {
'type': 'array',
'items': t2s(xsd_type.elements[0][1].type),
}
else:
schema = t2s(xsd_type.elements[0][1].type)
elif isinstance(xsd_type, zeep.xsd.ComplexType):
properties = collections.OrderedDict()
schema = {
'type': 'array',
'items': self.type2schema(xsd_type.elements[0][1].type, compress=compress),
'type': 'object',
'properties': properties,
'$anchor': to_id(type_name),
}
for key, element in xsd_type.elements:
if element.min_occurs > 0:
schema.setdefault('required', []).append(key)
element_schema = t2s(element.type)
if element.max_occurs == 'unbounded' or element.max_occurs > 1:
element_schema = {'type': 'array', 'items': element_schema}
properties[key] = element_schema
if not properties:
schema = {'type': 'null'}
elif isinstance(xsd_type, zeep.xsd.BuiltinType):
schema = {'type': 'string'}
else:
schema = self.type2schema(xsd_type.elements[0][1].type, compress=compress)
elif isinstance(xsd_type, zeep.xsd.ComplexType):
properties = collections.OrderedDict()
schema = {
'type': 'object',
'properties': properties,
}
for key, element in xsd_type.elements:
if element.min_occurs > 0:
schema.setdefault('required', []).append(key)
element_schema = self.type2schema(element.type, compress=compress)
if element.max_occurs == 'unbounded' or element.max_occurs > 1:
element_schema = {'type': 'array', 'items': element_schema}
properties[key] = element_schema
if not properties:
schema = {'type': 'null'}
elif isinstance(xsd_type, zeep.xsd.BuiltinType):
schema = {'type': 'string'}
else:
schema = {}
if xsd_type.qname:
schema['description'] = str(xsd_type.qname).replace('{http://www.w3.org/2001/XMLSchema}', 'xsd:')
return schema
schema = {}
if xsd_type.qname:
schema['description'] = str(xsd_type.qname).replace(
'{http://www.w3.org/2001/XMLSchema}', 'xsd:'
)
return schema
return t2s(xsd_type)

View File

@ -109,8 +109,10 @@ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
</soap:Body>
</soap:Envelope>'''
INPUT_SCHEMA = {
'$anchor': 'ref-6adf97f83acf6453d4a6a4b1070f3754',
'properties': {
'firstName': {
'$anchor': 'ref-dbd3a37522045c54032a5b96864a500d',
'description': '{http://www.examples.com/wsdl/HelloService.wsdl}firstName',
'properties': {
'string': {'items': {'type': 'string', 'description': 'xsd:string'}, 'type': 'array'},
@ -124,6 +126,7 @@ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
'type': 'object',
}
OUTPUT_SCHEMA = {
'$anchor': 'ref-6adf97f83acf6453d4a6a4b1070f3754',
'properties': {
'greeting': {'type': 'string', 'description': 'xsd:string'},
'who': {'type': 'string', 'description': 'xsd:string'},
@ -157,6 +160,11 @@ class SOAP12(SOAP11):
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="urn:examples:helloservice"
targetNamespace="urn:examples:helloservice">
<xsd:complexType name="recurse">
<xsd:sequence>
<xsd:element name="anotherme" type="tns:recurse" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="sayHello">
<xsd:complexType>
<xsd:sequence>
@ -170,6 +178,7 @@ class SOAP12(SOAP11):
<xsd:sequence>
<xsd:element name="greeting" type="xsd:string"/>
<xsd:element name="who" type="xsd:string" maxOccurs="unbounded"/>
<xsd:element name="recursion" type="tns:recurse" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
@ -223,10 +232,12 @@ class SOAP12(SOAP11):
<sayHelloResponse xmlns="urn:examples:helloservice">
<greeting>Hello</greeting>
<who>John!</who>
<recursion><anotherme/></recursion>
</sayHelloResponse>
</soap:Body>
</soap:Envelope>'''
INPUT_SCHEMA = {
'$anchor': 'ref-5b712371a9c9bf61f983831c2ed3f364',
'type': 'object',
'properties': {
'firstName': {'type': 'array', 'items': {'type': 'string', 'description': 'xsd:string'}},
@ -236,6 +247,7 @@ class SOAP12(SOAP11):
'description': '{urn:examples:helloservice}sayHello',
}
OUTPUT_SCHEMA = {
'$anchor': 'ref-5e505e086d14d5417f2799da5c085712',
'description': '{urn:examples:helloservice}sayHelloResponse',
'properties': {
'greeting': {'type': 'string', 'description': 'xsd:string'},
@ -243,6 +255,16 @@ class SOAP12(SOAP11):
'type': 'array',
'items': {'type': 'string', 'description': 'xsd:string'},
},
'recursion': {
'$anchor': 'ref-63d3d62358d2daf62cd2ebd07640165e',
'description': '{urn:examples:helloservice}recurse',
'type': 'object',
'properties': {
'anotherme': {
'$ref': '#ref-63d3d62358d2daf62cd2ebd07640165e',
}
},
},
},
'required': ['greeting', 'who'],
'type': 'object',
@ -255,6 +277,7 @@ class SOAP12(SOAP11):
OUTPUT_DATA = {
'greeting': 'Hello',
'who': ['John!'],
'recursion': {'anotherme': None},
}
VALIDATION_ERROR = 'Expected at least 1 items (minOccurs check) 0 items found. (sayHello.firstName)'