290: SOAP With Savon
(view original Railscast)
There comes a time in every web developer’s life when they have to communicate with a SOAP API; when simple Ruby code needs to talk with complex .Net and Java applications. Thankfully there’s help for this potentially unpleasant task. Savon is a Ruby gem written by Daniel Harrington that provides a nice Ruby interface for communicating with SOAP APIs. In this episode we’ll show you how it works.
ZipCode Lookups
Below is a page from an application with a simple form that takes a Zip code. We want users to be able to enter a Zip code and have information it returned.
As you can see our application doesn’t work yet. We have placeholders for the information but nothing is shown as we need to communicate with an external Web Service to get this information. We’ll get this information from the WebserviceX.NET website. This site has a SOAP API that can provide a lot of useful information such as stock market quotes, currency conversion, weather information and a whole lot more and we’ll use it to fetch information about a Zip code. The site has a page with a form we can use to test the web service. If we enter a valid zip code in the form we’ll set some XML returned containing data about that Zip code. If, for example, we enter 90210 we’ll get this response.
<NewDataSet> <Table> <CITY>Beverly Hills</CITY> <STATE>CA</STATE> <ZIP>90210</ZIP> <AREA_CODE>310</AREA_CODE> <TIME_ZONE>P</TIME_ZONE> </Table> </NewDataSet>
Using soapUI To Test SOAP calls
Before we jump right in and use a SOAP API in an application it’s a good idea to experiment with it a little to make sure that it works the way we expect it to. To help with this we’ll use a Java application called soapUI that we can use to test Web Services. Once we’ve installed it we can run it and create a new project. When we do we’ll be shown a dialog box that asks for a WSDL URL, among other things. The page we viewed before has this information and the URL we need to enter is http://www.webservicex.net/uszip.asmx?WSDL
.
A WSDL is like a blueprint and it tells us they actions that are available at that URL. Once we’ve entered the WSDL URL into soapUI we’ll see a list of the actions. If we expand the GetInfoByZip action and click the “Request 1” field that’s revealed we’ll see the XML that’s required for making a request.
This is the information we need to get from soapUI so that we can compare it to Savon as we use it in our Rails application. If we replace the question mark in the <web:USZip>
element with a real Zip code and click the green arrow we see the XML response.
Getting Started With Savon
Now that we have a way of testing SOAP requests we can start using Savon in our application. As is usual when installing a gem we do this by adding the gem to the Gemfile and then running bundle.
/Gemfile
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'savon'
To get a feel for how Savon works we’ll experiment with it in the console. The first thing we need to do is create a new Savon::Client, passing it the URL we want to to communicate with.
> client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL")
We can provide a block here if we want to provide additional configuration, but for our purposes this is enough. Next we’ll get a list of the actions.
> client.wsdl.soap_actions HTTPI executes HTTP GET using the net_http adapter => [:get_info_by_zip, :get_info_by_city, :get_info_by_state, :get_info_by_area_code]
One of the actions that is returned is just the one we’re looking for. (Note that Savon has changed the name from GetInfoByZip
to the more Ruby-like get_info_by_zip
).
We make a request by calling client.request
and passing in the action we want to call. We can also pass in a namespace as an optional first argument. If we look at the XML that was generated by soapUI we’ll see that a web
namespace is used so we’ll need to pass that in. Finally we’ll need to add some options. Again, we could use a block here, but instead we’ll pass in a body
option. This option takes a hash and it’s here where we pass the parameters in that we saw in the XML request. In soapUI the Zip code was passed in in a <web:USZip>
element. We’ve already specified the namespace so we just need to pass in the Zip code as us_zip
. (Again, Savon changes the capitalization of the parameter.
> client.request :web, :get_info_by_zip, body: { us_zip: "90210" }
We’ll see a large response from this request. This usually goes to the development log file so if we need to debug SOAP calls we can look there to see what the request and response were. The important parts of the response are the request XML and response XML and if we look at the response we’ll see that it doesn’t contain the information we were expecting.
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetInfoByZIPResponse xmlns="http://www.webserviceX.NET" /> </soap:Body> </soap:Envelope>
It looks like something’s wrong with our request. The problem in this case is that the casr of the us_zip code parameter is incorrect. Parameters are case sensitive and if we look at the relevant part of the request XML we’ll see that the USZip parameter was sent as usZip instead.
<web:usZip>90210</web:usZip>
By default Savon uses lowerCamelCase for parameters, but if we pass the name in as a string instead of as a symbol the capitalization will be preserved.
> client.request :web, :get_info_by_zip, body: { "USZip" => "90210" }
When we send the request this time the response contains the information we need.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetInfoByZIPResponse xmlns="http://www.webserviceX.NET"> <GetInfoByZIPResult> <NewDataSet xmlns=""> <Table> <CITY>Beverly Hills</CITY> <STATE>CA</STATE> <ZIP>90210</ZIP> <AREA_CODE>310</AREA_CODE> <TIME_ZONE>P</TIME_ZONE> </Table> </NewDataSet> </GetInfoByZIPResult> </GetInfoByZIPResponse> </soap:Body> </soap:Envelope>
The last command we sent returns a response object and we can assign this to a variable in the console by using an underscore to get the last thing returned.
> response = _
Calling to_hash on this object gives us a Ruby hash that’s easy to parse.
> response.to_hash => {:get_info_by_zip_response=> {:get_info_by_zip_result=> {:new_data_set=>{:table=> {:city=>"Beverly Hills", :state=>"CA", :zip=>"90210", :area_code=>"310", :time_zone=>"P"}, :@xmlns=>""}}, :@xmlns=>"http://www.webserviceX.NET"}}
The information we need is fairly deeply nested in the hash but we can still get at it fairly easily. Now that we know how Savon works we can start to use it in our Rails application.
Using Savon in a Rails Application
Our application has a ZipCode
model class. This isn’t an ActiveRecord model just a Ruby class with some attributes and an initializer that takes a Zip code.
/app/models/zip_code.rb
class ZipCode attr_reader :state, :city, :area_code, :time_zone def initialize(zip) end end
When a new ZipCode
object is instantiated we want to set the attributes with values from the SOAP call. The code we’ll need to add to the class is similar to that we ran in the console.
/app/models/zip_code.rb
class ZipCode attr_reader :state, :city, :area_code, :time_zone def initialize(zip) client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL") response = client.request :web, :get_info_by_zip, body: { "USZip" => zip } data = data = response.to_hash[:get_info_by_zip_response][:get_info_by_zip_result][:new_data_set][:table] @state = data[:state] @city = data[:city] @area_code = data[:area_code] @time_zone = data[:time_zone] end end
This code makes a request and calls to_hash
on the response. We then go into that deeply-nested hash and set the class’s attributes to the relevant parts of it. We can try this out now. When we enter a Zip code and click “Lookup” the SOAP call is made and we’ll see the information on the page so our code is working.
Handling Errors
We’ll need to modify out code so that it handles errors. As it stands if we enter an invalid Zip code and try to look it up the application raises an exception as the response doesn’t have the deeply-nested hash we’re expecting.
One way to handle these cases is to check that the response is successful before trying to set the attributes. We could use response.success?
to check this and while this is useful, it won’t help in this case as the response is successful when an invalid Zip code is entered, the problem is that the response is empty.
To help us here we can use Savon’s to_array
method here instead of to_hash
. We can pass this a list of hash keys and rather than throwing an exception if the nesting we’re looking for doesn’t exist it will return an empty array. As an array is returned we need to call first
to get the results we want. If there are no results we’ll get nil
so we can check for that before setting the attributes.
/app/models/zip_code.rb
def initialize(zip) client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL") response = client.request :web, :get_info_by_zip, body: { "USZip" => zip } if response.success? data = response.to_array(:get_info_by_zip_response, :get_info_by_zip_result, :new_data_set, :table).first if data @state = data[:state] @city = data[:city] @area_code = data[:area_code] @time_zone = data[:time_zone] end end end
If we reload the page now we’ll see an empty result rather than an exception.
There’s more work we can do here to show an error message when an invalid Zip code is entered but we won’t do that here.
Some Final Tips
We’ll finish this episode with a couple of tips. If you find yourself having to use strings a lot because the first part of a tag name is capitalized you can add this line of code to our application to make the tags use UpperCamelCase rather than the default lowerCamelCase. If the API you’re talking to uses this kind of casing this means that you can use symbols instead of strings in the arguments.Gyoku.convert_symbols_to :camelcase
A WSDL file shouldn’t change often so so it’s a good idea to cache it rather than downloading it every time. We can download the WSDL file, store it locally and then use a file reference when we create a new Savon::Client
rather than a web reference.
That’s it for our episode on Savon. Savon makes it much easier to communicate with a SOAP API. If you use it in your applications there are a couple of related projects that you might find useful. The first of these is Savon Model. This provides a nice DSL for setting up the client inside a class and it would probably work well inside our ZipCode
class and allow us to clean up the code there.
The other project is Savon Spec. This can help in testing by mocking the SOAP requests. This is fine for lower-level tests but to test thoroughly you should also have some higher level integration tests that test hitting that actual API. This way if the API changes the tests will break. The VCR gem can help greatly with this and there’s more information about VCR in this week’s pro episode.