Implementing SOAP in Perl today
I started looking at SOAP around 2001, and at that time SOAP::Lite was the only viable option to both consume and produce SOAP services in Perl. Everybody knew SOAP::Lite was a mess, but it was pretty much what we got and we were stuck with it.
In 2001 SOAP::Lite wasn't much a problem and that's because it implements the RPC/Encoded format for SOAP messages, and by that time this was pretty much the default. But the usage of SOAP changed, a lot, and the module didn't catch up with that changes.
First of all, we need to understand that SOAP is much more than a way to do RPC, if you need plain RPC without much management, then XML::RPC or JSON::RPC is just fine and will work without any concerns. But if, on the other hand, you need a strict management of the data being transfered as well as a proper documenting of which services use which data types, then SOAP is probably the thing you need.
But for using SOAP, there are two aspects you need to undestand:
Style
The style describes the general semantics of the service. There are two different styles in SOAP:
- Document: This style is used for services that represent the submission of some arbitrary data that expects a result or not. One example of a service where Document style makes sense is to submit a purchase in a B2B system.
- RPC: This style is used where you're acessing a procedural or oriented object API of some sort, its semantics define that one specific resource support several operations.
The biggest practical difference is that in the Document Style, you're going to submit one or more XML documents as the message, and this document is the resource to be used by that specific operation. In the RPC style, the first and only child of the body element describes which is the operation that this message is trying to execute, and the parameters for that operation are the direct child of that main element.
Body Use
Indpendent of the style of the service, the other aspect that governs how SOAP works is the "use" of the body. There are two types of "use"
- Encoded: This referes to SOAP-Encoding, which is a specific format for data serialization described by the SOAP protocol itself. In the early usage of SOAP, this was the main way of exchanging data.
- Literal: This defines that the content of the message is actually encoded acording to some arbitrary XML Schema, this has the advantage of being able to represent any data that XML can, and also makes the serialization format more decoupled from the language providing the service.
Style/Use
Considering all that, there are four ways of using SOAP:
- RPC/Encoded: This is the only format supported by SOAP::Lite, and hardly used in new services.
- RPC/Literal: This allows you to use arbitrary XML data as the parameters and response for an API that you want to expose.
- Document/Encoded: While this mix is theoretically possible, I have never heard about any use of it.
- Document/Literal: This is a type of service where you have only one operation per endpoint, and usually don't map well to RPC semantics, since it would require several endpoints to implement the whole API.
WARNING: Microsoft created a pseudo-standard called Document/Literal-Wrapped which is a mix of both RPC/Literal and Document/Literal. The service is described with Document/Literal, but the XML Schemas of the service are made considerably more complex to specify the wrapper element that would be otherwise natural in the RPC Style. It also requires the use of the SOAP-Action header to define which operation is being called. The SOAP-Action header is HTTP specific and was supposed to be used for routing purposes, not to define which is the operation being invoked. Please use RPC/Literal when you need RPC semantics with Literal body use, although I have implemented a compatibility mode for this aberration, it should not be promoted in any way.
Finally, let's implement some SOAP.
The first thing you need to do when implementing a SOAP service is deciding wether your service has RPC or Document semantics. In our example, I'm going to implement a Document-style service "converter", which receives a document describing the amount, the origin unit and the target unit, returning a document with the target amount set.
An example of a RPC style service would be an API that would include things like: get_available_source_units(), get_available_target_units() and other correlated operations. In our case we are going to assume that the document itself provides all the information we need and that the associated metadata (the XML Schema) will provide us sane data.
The second think you need to do is describing your data in the XML Schema format, so for our "unit converter" service, we're going to have a UnitConversion element. If you're used to XML, you know that you need a namespace URI for your data and your services, I'm going to use http://daniel.ruoso.com/categoria/perl/soap-today as the namespace for both the data and the service.
<xsd:schema elementFormDefault="qualified"
xsd:targetNamespace="http://daniel.ruoso.com/categoria/perl/soap-today"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:unit="http://daniel.ruoso.com/categoria/perl/soap-today">
<xsd:element name="UnitConversion">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="input">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="amount" type="xsd:double" />
<xsd:element name="unit" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="output">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="amount" type="xsd:double" />
<xsd:element name="unit" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
The above XML Schema will support data that looks like:
<UnitConversion>
<input>
<amount>10</amount>
<unit>kg</unit>
</input>
<output>
<unit>pound</unit>
</output>
</UnitConversion>
Now that you specified how your data looks like, you need to specify how your service looks like that is done using the Web Service Description Language -- WSDL. Basically, you define:
- Messages: The messages describes a single message exchange, independent of the Message Exchange Pattern. The WS-I says that you should have "Request" and "Response" as part of the message names, but I disagree, since a message might be used for both request and response.
- Port Type: The Port Type describes the interface of the service, independent of the transport, basically grouping the messages into a specific "Message Exchange Pattern" -- usually this is request/response, but you can have both request-only and reponse-only.
- Binding: The binding associates a given port type with some transport mechanism and describes the attributes of how that message will be transported. It's in the binding that you'll describe you want to use SOAP as well as the style/use.
- Service: The service groups a set of bindings defining where are the endpoints for the client to access the services.
I'm not going to explain each detail of the WSDL here, but I think it's pretty straight forward. WSDLs are complex when generated by tools like .Net, but it doesn't need to be. This WSDL assumes that the previous XML Schema is saved as unit-conversion.xsd.
<wsdl:definitions
xmlns:http="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:unit="http://daniel.ruoso.com/categoria/perl/soap-today"
targetNamespace="http://daniel.ruoso.com/categoria/perl/soap-today">
<import namespace="http://daniel.ruoso.com/categoria/perl/soap-today"
uri="unit-conversion.xsd" />
<wsdl:message name="ConvertUnit">
<part name="UnitConversion" element="unit:UnitConversion" />
</wsdl:message>
<wsdl:portType name="ConvertUnit">
<wsdl:operation name="convert_unit>
<wsdl:input message="unit:ConvertUnit" />
<wsdl:output message="unit:ConvertUnit" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="ConvertUnitSOAPHTTP" type="unit:ConvertUnit">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<wsdl:operation name="convert_unit>
<wsdl:input>
<soap:body use="literal">
</wsd:input>
<wsdl:output>
<soap:body use="literal">
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="ConvertUnitService">
<wsdl:port name="ConvertUnit" binding="unit:ConvertUnitSOAPHTTP">
<soap:address location="http://localhost/myservice" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
Ok, now let's really implement it
Now that we're gone through all the overhead of SOAP (and I say it again: if you think this overhead is overkill, go use XML::RPC or JSON::RPC instead, but if you need the services to be strictly documented, SOAP is what you need) we can implement the service itself. And for that we're going to use Catalyst::Controller::SOAP.
I'm not going to explain how to create a Catalyst application, there are lots of tutorials on how to do it, and if you're still reading this, you're probably aware of Catalyst already. So, after you create your Catalyst application, you need a controller that will implement the service, all you need to do is subclass Catalyst::Controller::SOAP and implements the service itself.
package MyApp::Controller::UnitConverter;
use strict;
use warnings;
use base qw(Catalyst::Controller::SOAP);
__PACKAGE__->config->{wsdl} =
{wsdl => '/usr/share/unit-converter/schemas/UnitConverter.wsdl',
schema => '/usr/share/unit-converter/schemas/unit-conversion.xsd'};
sub convert_unit :WSDLPort('ConvertUnit') {
my ($self, $c, $unit_conversion) = @_;
my $data = $unit_conversion->{UnitConversion};
if ($data->{input}{unit} eq 'kg' &&
$data->{output}{unit} eq 'pounds') {
$data->{output}{amount} =
2.20462262 * $data->{input}{amount}
$c->stash->{soap}->compile_return($unit_conversion);
} else {
$c->stash->{soap}->fault({ code => 'SOAP-ENV:Client', reason => 'unsupported' });
}
}
1;
And voilá! You have a SOAP service running. Please refer to the Catalyst::Controller::SOAP docs for more information on the details or just ask me, either in the comments or at #catalyst@irc.perl.org
Syndicated 2009-07-20 15:23:05 from Daniel Ruoso