We've recently started to use Spyne and it seems very powerful. In our case, we need to replicate a legacy SOAP API that we're substituting. In order to ensure compatibility with the current clients, we want to get the spyne-generated WSDL to be as close a match as possible to current WSDL.
As was noted in this SO question:
How can I stop Spyne from wrapping arguments in a complexType?
Spyne wraps the incoming parameters for methods with multiple simple parameters in a ComplexType structure.
In the legacy wsdl, this operation
<operation name="UCDprovision_password" parameterOrder="p_msisdn p_pin p_password">
<input name="UCDprovision_passwordRequest" message="impl:UCDprovision_passwordRequest"/>
<output name="UCDprovision_passwordResponse" message="impl:UCDprovision_passwordResponse"/>
</operation>
is related to this message:
<message name="UCDprovision_passwordRequest">
<part name="p_msisdn" type="xsd:string"/>
<part name="p_pin" type="xsd:string"/>
<part name="p_password" type="xsd:string"/>
</message>
and this ComplexType:
<complexType name="ObjResultadoBasico">
<sequence>
<element name="PError" type="xsd:int"/>
<element name="SError" nillable="true" type="xsd:string"/>
</sequence>
</complexType>
We have created the following spyne code to replicate the operation:
class ObjResultadoBasico(ComplexModel):
PError = Integer
SError = String
class UCDService(ServiceBase):
@rpc(String, String, String, _returns=ObjResultadoBasico, _in_message_name='UCDprovision_passwordRequest')
def UCDprovision_password(ctx,p_msisdn,p_pin,p_password):
return ObjResultadoBasico('1',p_msisdn)
And this produces the following WSDL (non-relevant parts omitted)
<xs:complexType name="ObjResultadoBasico">
<xs:sequence>
<xs:element name="PError" type="xs:integer" minOccurs="0" nillable="true"/>
<xs:element name="SError" type="xs:string" minOccurs="0" nillable="true"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="UCDprovision_passwordRequest">
<xs:sequence>
<xs:element name="p_msisdn" type="xs:string" minOccurs="0" nillable="true"/>
<xs:element name="p_pin" type="xs:string" minOccurs="0" nillable="true"/>
<xs:element name="p_password" type="xs:string" minOccurs="0" nillable="true"/>
</xs:sequence>
</xs:complexType>
<wsdl:operation name="UCDprovision_password" parameterOrder="UCDprovision_passwordRequest">
<wsdl:input name="UCDprovision_passwordRequest" message="tns:UCDprovision_passwordRequest"/>
<wsdl:output name="UCDprovision_passwordResponse" message="tns:UCDprovision_passwordResponse"/></wsdl:operation>
The difference we're trying to work on is the ComplexType wrapping the UCDprovision_passwordRequest which was detailed as a in the legacy WSDL.
EDIT: Adding the output when the request message is pre-defined in its own complex type and the operation itself can therefore use "bare" style, using the code suggested by Burak Arslan below:
<xs:complexType name="UCDprovision_passwordRequest">
<xs:sequence>
<xs:element name="p_msisdn" type="xs:string" minOccurs="0" nillable="true"/>
<xs:element name="p_pin" type="xs:string" minOccurs="0" nillable="true"/>
<xs:element name="p_password" type="xs:string" minOccurs="0" nillable="true"/>
</xs:sequence>
</xs:complexType>
<xs:element name="UCDprovision_password" type="tns:UCDprovision_passwordRequest"/>
<wsdl:message name="UCDprovision_password">
<wsdl:part name="UCDprovision_password" element="tns:UCDprovision_password"/>
</wsdl:message>
The problem with this solution is that it's not the operation itself that I don't want wrapped, but the input message to that operation. I assume that this the result because UCDprovision_passwordRequest, the input message, is declared as a ComplexModel.
Using SOAPUI, these are examples of the requests:
Old WSDL
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ucd="http://UCD">
<soapenv:Header/>
<soapenv:Body>
<ucd:UCDprovision_pin>
<p_msisdn>?</p_msisdn>
<p_usuario>?</p_usuario>
<p_nemonico>?</p_nemonico>
<p_pin>?</p_pin>
</ucd:UCDprovision_pin>
</soapenv:Body>
</soapenv:Envelope>
Spyne WSDL
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ucd="UCD">
<soapenv:Header/>
<soapenv:Body>
<ucd:UCDprovision_pin>
<ucd:p_msisdn>?</ucd:p_msisdn>
<ucd:p_usuario>?</ucd:p_usuario>
<ucd:p_nemonico>?</ucd:p_nemonico>
<ucd:p_pin>?</ucd:p_pin>
</ucd:UCDprovision_pin>
</soapenv:Body>
</soapenv:Envelope>
At the end of the day, as you can see, from the client's point of view only the namespace of the parameters is different, and that is probably fixable (I haven't tried, how would you do that?).
Since we just need to accept what the client sends with the old WSDL, I have tested "soft" validation and the request with the old WSDL is accepted by Spyne code (which generates the new WSDL).
We'll continue testing, but I think in the end, unless not recommended, I'll just accept the ComplexType definitions and use "soft" validation, which solves our problem. The only question would be how to create the WSDL without the namespace in the ComplexType attributes
EDIT: The following text is irrelevant if no patch is needed.
As answered in the reference question above, if we only had one in_parameter we could use _body_type='bare' in the rpc decorator to avoid this, but this is not allowed in spyne for 0 or more than 1 parameter. I'm sure there are valid reasons for that, of course.
All that said, and with the objective of replicating the original WSDL as much as possible, we would like to develop a patch to add the option for this multi-parameters to be represented as in the WSDL. If possible, we would also like to contribute this patch upstream, if we manage a good enough implementation.
And now, to the question: What would be the best way to approach this patch?
In a first review, the code where this XML segment is created is this, in decorator.py:
message = None
if body_style_str == 'bare':
if len(in_params) > 1:
raise LogicError("body_style='bare' can handle at most one "
"function argument.")
if len(in_params) == 0:
message = ComplexModel.produce(type_name=in_message_name,
namespace=ns, members=in_params)
else:
message, = in_params.values()
message = message.customize(sub_name=in_message_name, sub_ns=ns)
if issubclass(message, ComplexModelBase) and not message._type_info:
raise LogicError("body_style='bare' does not allow empty "
"model as param")
# there can't be multiple arguments here.
if message.__type_name__ is ModelBase.Empty:
message._fill_empty_type_name(ns, in_message_name,
"%s_arg0" % in_message_name)
else:
message = ComplexModel.produce(type_name=in_message_name,
namespace=ns, members=in_params)
message.__namespace__ = ns
Would it be enough for us to add an optional flag to the decorator and use that flag to create the message object as we want, instead of going the ComplexType route? A la:
if len(in_params) > 1:
if (parameters_as_message):
message = message.special_way_to_construct(type_name=in_message_name,
namespace=ns, members=in_params)
else:
raise LogicError("body_style='bare' can handle at most one "
"function argument.")
Thanks for reading up to this point, and if Burak Arslan reads this, thanks for all your work!