Savon

Heavy metal SOAP client

Now for the fun part. To execute SOAP requests, you use the Savon::Client#request method. Here's a very basic example of executing a SOAP request to a get_all_users action.

response = client.request :get_all_users

This single argument (the name of the SOAP action to call) works in different ways depending on whether you're using a WSDL document. If you do, Savon will parse the WSDL document for available SOAP actions and convert their names to snake_case Symbols for you.

Savon converts snake_case_symbols to lowerCamelCase like this:

:get_all_users.to_s.lower_camelcase  # => "getAllUsers"
:get_pdf.to_s.lower_camelcase        # => "getPdf"

This convention might not work for you if your service requires CamelCase method names or methods with UPPERCASE acronyms. But don't worry. If you pass in a String instead of a Symbol, Savon will not convert the argument. The difference between Symbols and String identifiers is one of Savon's convention.

response = client.request "GetPDF"

The argument(s) passed to the #request method will affect the SOAP input tag inside the SOAP request.
To make sure you know what this means, here's an example for a simple request:

<env:Envelope
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <env:Body>
    <getAllUsers />  <!-- the SOAP input tag -->
  </env:Body>
</env:Envelope>

Now if you need the input tag to be namespaced <wsdl:getAllUsers />, you pass two arguments to the #request method. The first (a Symbol) will be used for the namespace and the second (a Symbol or String) will be the SOAP action to call:

response = client.request :wsdl, :get_all_users

You may also need to bind XML attributes to the input tag. In this case, you pass a Hash of attributes following to the name of your SOAP action and the optional namespace.

response = client.request :wsdl, "GetPDF", id: 1

These arguments result in the following input tag.

<wsdl:GetPDF id="1" />

Wrestling with SOAP

To interact with your service, you probably need to specify some SOAP-specific options. The #request method is the second important method to accept a block and lets you access the following objects.

[soap, wsdl, http, wsse]

Notice, that the list is almost the same as the one for Savon.client. Except now, there is an additional object called soap. In contrast to the other three objects, the soap object is tied to single requests.

Savon::SOAP::XML (soap) can only be accessed inside this block and Savon creates a new soap object for every request.

Savon by default expects your services to be based on SOAP 1.1. For SOAP 1.2 services, you can set the SOAP version per request.

response = client.request :get_user do
  soap.version = 2
end

If you don't pass a namespace to the #request method, Savon will attach the target namespaces to "xmlns:wsdl". If you pass a namespace, Savon will use it instead of the default.

client.request :v1, :get_user
<env:Envelope
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:v1="http://v1.example.com">
  <env:Body>
    <v1:GetUser>
  </env:Body>
</env:Envelope>

You can always set namespaces and overwrite namespaces. They're stored as a Hash.

# setting a namespace
soap.namespaces["xmlns:g2"] = "http://g2.example.com"

# overwriting "xmlns:wsdl"
soap.namespaces["xmlns:wsdl"] = "http://ns.example.com"

A little interaction

To call the get_user action of a service and pass the ID of the user to return, you can use a Hash for the SOAP body.

response = client.request :get_user do
  soap.body = { id: 1 }
end

If you only need to send a single value or if you like to create a more advanced object to build the SOAP body, you can pass any object that's not a Hash and responds to to_s.

response = client.request :get_user_by_id do
  soap.body = 1
end

As you already saw before, Savon is based on a few conventions to make the experience of having to work with SOAP and XML as pleasant as possible. The Hash is translated to XML using Gyoku which is based on the same conventions.

soap.body = {
  :first_name => "The",
  :last_name  => "Hoff",
  "FAME"      => ["Knight Rider", "Baywatch"]
}

As with the SOAP action, Symbol keys will be converted to lowerCamelCase and String keys won't be touched. The previous example generates the following XML.

<env:Envelope
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:wsdl="http://v1.example.com">
  <env:Body>
    <wsdl:CreateUser>
      <firstName>The</firstName>
      <lastName>Hoff</lastName>
      <FAME>Knight Rider</FAME>
      <FAME>Baywatch</FAME>
    </wsdl:CreateUser>
  </env:Body>
</env:Envelope>

Some services actually require the XML elements to be in a specific order. If you don't use Ruby 1.9 (and you should), you can not be sure about the order of Hash elements and have to specify the correct order using an Array under a special :order! key.

{
  :last_name  => "Hoff",
  :first_name => "The",
  :order!     => [:first_name, :last_name]
}

This will make sure, that the lastName tag follows the firstName.

Assigning arguments to XML tags using a Hash is even more difficult. It requires another Hash under an :attributes! key containing a key matching the XML tag and the Hash of attributes to add.

{
  :city        => nil,
  :attributes! => { :city => { "xsi:nil" => true } }
}

This example will be translated to the following XML.

<city xsi:nil="true"></city>

I would not recommend using a Hash for the SOAP body if you need to create complex XML structures, because there are better alternatives. One of them is to pass a block to the Savon::SOAP::XML#body method. Savon will then yield a Builder::XmlMarkup instance for you to use.

soap.body do |xml|
  xml.firstName("The")
  xml.lastName("Hoff")
end

Last but not least, you can also create and use a simple String (created with Builder or any another tool):

soap.body = "<firstName>The</firstName><lastName>Hoff</lastName>"

Besides the body element, SOAP requests can also contain a header with additional information. Savon sees this header as just another Hash following the same conventions as the SOAP body Hash.

soap.header = { "SecretKey" => "secret" }

If you're sure that none of these options work for you, you can completely customize the XML to be used for the SOAP request.

soap.xml = "<custom><soap>request</soap></custom>"

The Savon::SOAP::XML#xml method also accepts a block and yields a Builder::XmlMarkup instance.

namespaces = {
  "xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/",
  "xmlns:blz" => "http://thomas-bayer.com/blz/"
}

soap.xml do |xml|
  xml.soapenv(:Envelope, namespaces) do |xml|
    xml.soapenv(:Body) do |xml|
      xml.blz(:getBank) do |xml|
        xml.blz(:blz, "24050110")
      end
    end
  end
end

Please take a look at the examples for some hands-on exercise.