In Progress
Unit 1, Lesson 21
In Progress

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

Bar of Soap

At some point in your time as a Ruby developer you might find yourself needing to interact with a SOAP API.

API Examples

You might run into a SOAP API when integrating services from companies like Salesforce and Mindbody, or from a slew of financial institutions

Bar of Soap

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

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.

WSDL

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.

SOAP

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.

Savon: Heavy metal SOAP client

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.

Currency Exchange

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...

Crying Cat

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.

Soap testing

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!

Smiling Cat

If you ever find yourself needing to use a SOAP API, don't forget how easy Savon makes it to test and implement.

Responses