Savon & SOAP APIs
Once upon a time we didn’t have GraphQL or RESTful JSON APIs. Once upon a time, if you wanted to make a request to a web service you had to use XML-based protocols such as XMLRPC or SOAP. There are a lot of services around that still speak those old protocols, and interfacing with them might feel daunting.
Fortunately, you don’t have to start from scratch. In today’s episode, guest chef Beth Haubert joins us to demonstrate how we can quickly create connections to SOAP-based legacy services from Ruby code. You’ll learn about the SAVON gem, how to dynamically query a SOAP API for its available methods, and how to create unit tests around your SOAP interfaces. Enjoy!
Video transcript & code
Savon & SOAP APIs
At some point in your time as a Ruby developer you might find yourself needing to interact with a SOAP API.
You might run into a SOAP API when integrating services from companies like Salesforce and Mindbody, or from a slew of financial institutions
If you had asked me what SOAP was six months ago, I would have told you it was the substance you use to wash your dishes, clothes, or any other manner of things, but after a recent project, I now know an alternative definition.
SOAP, which stands for Simple Object Access Protocol, is an XML-based API protocol that preceded JSON. It's comparable to the JSON API definition. But SOAP uses something called a WSDL to define the rules for how a request should be structured -- down to the details of what the items in the request must be named.
A WSDL, or Web Services Description Language, defines the functions that you can implement or expose to the client. A WSDL is how you would write a formal api contract that you would expect a machine to read, a lot like a database schema. Humans can read it, but it's intended to be read by a computer.
Unfortunately for me, SOAP was not as simple to work with as its name implies. It doesn't return a very readable result. Performance is not great and it's not as easy to integrate with existing applications when compared to REST.
Luckily for us Ruby developers, there's the gem, Savon, to help us incorporate a third-party SOAP API into our application. Let me show you how it works.
So, we're building an application that converts a value from one currency to another.
require 'savon'
First, let's require the Savon gem.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
Next, we assign the url of the WSDL to a handy constant.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "1602B706F89F4376B7D75596FDC2903D"
}
}
Then we need to construct a header which includes a namespace and username, which in this example, is actually just our api key.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "1602B706F89F4376B7D75596FDC2903D"
}
}
client = Savon.client(wsdl: WSDL, soap_header: SOAP_HEADER)
Now we create the new savon client, passing in the WSDL URL and the header constant.
client.operations
The SOAP API we're using has many different functions. Savon gives me the capability of seeing all the possible methods as defined in the WSDL.
[:list_active_currencies,
:convert_real_time_value,
:get_historical_rates_range,
:get_real_time_rate,
:get_real_time_rates,
:get_real_time_rate_table,
:get_forward_rate,
:get_all_real_time_rates,
:list_currencies,
:get_latest_cross_rate,
:get_latest_cross_rates,
:get_best_cross_rate,
:get_best_cross_rates,
:convert_historical_value,
:get_historical_rate,
:get_latest_historical_rate,
:get_latest_historical_rates,
:get_historical_rates,
:get_historical_rates_ranges,
:get_official_historical_rate,
:get_official_rate,
:get_official_rates,
:get_official_historical_rates,
:get_tick,
:get_ticks,
:get_bar,
:get_bars,
:get_chart_bars,
:get_london_historical_rates_range,
:list_official_rates]
Here's the response we get back. Up near the top, that convert_real_time_value
looks useful. Unfortunately SAVON doesn't give us a way to see the parameters we need to pass through for each operation. Hopefully the SOAP API we're using has good documentation...
But if it doesn't, we can always fall back to looking directly at the WSDL...
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
...which is located at this URL.
According to the WSDL of this currency API, we need to pass in three arguments: "From", "To", and "Amount", which Savon lets us pass through as a message hash.
THe WSDL also defines what options we can use as the argument values. Let's put it all together.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "698A46ABEF944C40B959EE2DA07CD2E5"
}
}
client = Savon.client(wsdl: WSDL, soap_header: SOAP_HEADER)
We've already required the Savon library, set our constants, and generated our client.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "698A46ABEF944C40B959EE2DA07CD2E5"
}
}
client = Savon.client(
wsdl: WSDL,
soap_header: SOAP_HEADER
)
params = {"From"=>"AUD", "To" => "INR", "Amount" => "300"}
Based on the WSDL, we'll create the parameters with the from
, to
, and amount
defined.
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "698A46ABEF944C40B959EE2DA07CD2E5"
}
}
client = Savon.client(
wsdl: WSDL,
soap_header: SOAP_HEADER
)
params = {"From"=>"AUD", "To" => "INR", "Amount" => "300"}
client.call(:convert_real_time_value, message: params)
Then we'll invoke the method using the value parameters.
Wow, that's a complicated response. How do we actually know what we're looking for? Let's just look at the body.
{:convert_real_time_value_response=>
{:convert_real_time_value_result=>
{:outcome=>"Success",
:identity=>"Header",
:delay=>"0.0109822",
:from_currency_symbol=>"AUD",
:from_currency_name=>"Australian dollar",
:to_currency_symbol=>"INR",
:to_currency_name=>"India Rupees",
:date=>"04/05/2019",
:time=>"8:59:20 PM",
:amount=>"300",
:result=>"14745.66",
:rate=>"49.1522"},
:@xmlns=>"http://www.xignite.com/services/"}}
Okay, that's a bit better!
response.body.dig(
:convert_real_time_value_response,
:convert_real_time_value_result,
:result,
)
Let's dig in to get the actual result
so
"14745.66"
which gives us this string.
Okay. So we've played around with the API and showed you a little bit about how SOAP works, but how are we going to test it? If we're using TDD (and hopefully we are), we're going to have to learn how to mock out these responses, but again, Savon makes it easy for us!
require 'rspec'
Say we're using RSpec.
require 'rspec'
require 'savon/mock/spec_helper'
We're going to include Savon's spec_helper in our test.
require 'rspec'
require 'savon/mock/spec_helper'
require './currency_converter'
describe CurrencyConverter do
include Savon::SpecHelper
end
And let's call our class CurrencyConverter. It doesn't exist yet, but it will soon.
require 'rspec'
require 'savon/mock/spec_helper'
require './currency_converter'
describe CurrencyConverter do
include Savon::SpecHelper
before(:all) { savon.mock! }
after(:all) { savon.unmock! }
end
We want to prevent Savon from making actual HTTP calls, and fortunately we have these handy mock
and unmock
methods that allow us to do that.
class CurrencyConverter
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "1602B706F89F4376B7D75596FDC2903D"
}
}
def initialize(from:, to:, amount:)
@client = Savon.client(
wsdl: WSDL,
soap_header: SOAP_HEADER
)
@from = from
@to = to
@amount = amount
end
end
Let's move our setup code from earlier into a our new CurrencyConverter class.
We’re going to need a fixture to simulate the SOAP API response. Let's reuse the body of the response we got back from the server earlier in this video.
Success
Header
0.015999
AUD
Australian dollar
INR
India Rupees
04/05/2019
300
14745.66
49.1522
Make sure it's correctly formatted as an XML file. It should look something like this.
require 'rspec'
require 'savon/mock/spec_helper'
require './currency_converter.rb'
describe CurrencyConverter do
include Savon::SpecHelper
before(:all) { savon.mock! }
after(:all) { savon.unmock! }
describe "#converted_amount" do
it "converts an amount from one currency to another" do
message = { "From" => "AUD", "To" => "INR", "Amount" => "300" }
fixture = File.read("spec/fixtures/currency_converter.xml")
savon.expects(:convert_real_time_value).with(message: message).returns(fixture)
service = CurrencyConverter.new(from: "AUD", to: "INR", amount: "300")
response = service.converted_amount
expect(response).to eq(14745.66)
end
end
end
Now let's use that fixture in our test.
Savon expectations are fairly straightforward. Pass the name of the SOAP operation we're expecting to be called, with or without a message, and have it return the fixture we've created.
class CurrencyConverter
require 'savon'
WSDL = "http://globalcurrencies.xignite.com/xGlobalCurrencies.asmx?WSDL"
SOAP_HEADER = {
"Header" => {
"@xmlns" => "http://www.xignite.com/services/",
"Username" => "698A46ABEF944C40B959EE2DA07CD2E5"
}
}
def initialize(from:, to:, amount:)
@client = Savon.client(
wsdl: WSDL,
soap_header: SOAP_HEADER
)
@from = from
@to = to
@amount = amount
end
def converted_amount
response = convert_real_time_value
dig_response(response).to_f
end
private
attr_reader :client, :from, :to, :amount
def convert_real_time_value
params = {
"From" => from,
"To" => to,
"Amount" => amount
}
client.call(:convert_real_time_value, message: params)
end
def dig_response(convert_real_time_value)
convert_real_time_value.body.dig(
:convert_real_time_value_response,
:convert_real_time_value_result,
:result,
)
end
end
We’re going to fast forward through a few of the TDD steps here and flesh out our code in the converter class in order to satisfy this spec. You can see here I've added a few methods to get us there.
rspec currency_converter_spec.rb
Now, let's run our test. It passes!
If you ever find yourself needing to use a SOAP API, don't forget how easy Savon makes it to test and implement.
Responses