Enterprise Integration Zone is brought to you in partnership with:

Daniel Doubrovkine (aka dB.) is one of the tallest engineers at Art.sy. He founded and exited a successful Swiss start-up in the 90s, worked for Microsoft Corp. in Redmond, specializing in security and authentication, dabbled in large scale social networking and ran a big team that developed an expensive Enterprise product in NYC. After turning open-source cheerleader a few years ago in the worlds of C++, Java and .NET, he converted himself to Ruby and has been slowly unlearning everything he learned in the last 15 years of software practice. Daniel has posted 46 posts at DZone. You can read more from them at their website. View Full User Profile

TaxCloud: SOAP Service Integration in Ruby

11.30.2012
| 3390 views |
  • submit to reddit

I’ve been working on the the tax_cloud gem for the past couple of days and am happy to announce version 0.2.0, released today. The gem was started by @tempelmeyer and is now a mature wrapper for the TaxCloud US Sales Tax calculation service.

This library is also a nice example of a generic SOAP client wrapper in Ruby. I wanted to point out several successful patterns for this integration, which I cannot take credit for, for the most part.

Error Handling

I borrowed error handling from @modetojoy’s Mongoid. Today someone said: “I had a bug in a spec and Durran told me how to fix it in an error message.” True story. To accomplish this we define a base error that holds the problem, summary and resolution. In tax_cloud’s case this is  TaxCloud::Errors::TaxCloudError paired with config/locales/en.yml, a locale file that does the error formatting. There’re two things to do in order for the error code to find the message: add the locale file to the load path, in tax_cloud.rb, and do a bit of formatting with I18n.

    I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")

    def translate(key, options)
        ::I18n.translate("#{BASE_KEY}.#{key}", { :locale => :en }.merge(options)).strip
    end


What does an error in tax_cloud look like?

    Problem:
      Missing configuration.
    Summary:
      TaxCloud requires an API login ID and key.
    Resolution:
      Create a TaxCloud merchant account at http://www.taxcloud.net.
      Add a website to your TaxCloud account.
      This will generate an API ID and API Key that you will need to use the service.
      Configure the TaxCloud gem. For example, add the following to `config/initializers/tax_cloud.rb`.
      TaxCloud.configure do |config|
       config.api_login_id = 'your_tax_cloud_api_login_id'
       config.api_key = 'your_tax_cloud_api_key'
       config.usps_username = 'your_usps_username' # optional
      end

Pretty awesome

Safe SOAP Requests The tax_cloud gem uses Savon to make SOAP requests. “Savon” is French for “Soap”, which confuses the French speakers like myself trying to explain that SOAP is Savon. Anyway, a client is initialized with its WSDL.
    module TaxCloud #:nodoc:
      class Client < Savon::Client
        def initialize
          super 'https://api.taxcloud.net/1.0/?wsdl'
        end
      end
    end

First, we need to add authentication to every request, which is required by the API. We can override request.
    def request(method, body = {})
        super method, :body => body.merge(auth_params)
    end
     
    def auth_params
    {
        'apiLoginID' => TaxCloud.configuration.api_login_id,
        'apiKey' => TaxCloud.configuration.api_key
    }
    end

Second, we want to handle SOAP errors, and give a detailed explanation for SOAP faults, Mongoid-style. This is your typical block with yield.
    def request(method, body = {})
      safe do
        super method, :body => body.merge(auth_params)
      end
    end
     
    def safe &block
      begin
        yield
      rescue Savon::SOAP::Fault => e
        raise TaxCloud::Errors::SoapError.new(e)
      end
    end


The complete code can be found in client.rb. The error itself is parsed in soap_error.rb – SOAP faults come in standard format.

Parsing Responses

We will now raise a good-looking exception on SOAP failures, but we still must protect ourselves from unexpected data or successful SOAP requests that return API errors. That possibility is the thing I detest most about SOAP (vs. REST) – it makes programming a client unnecessarily complicated. The TaxCloud service returns a SOAP body with different values in key_response/key_result/response_type, where the key will be the name of the method invoked (eg. ping_response). A bit of meta-programming can make a base class, which can parse a response and match an XML path, raising errors where appropriate. It can be subclassed into a generic response type and, finally, into specific declarative implementations such as ping or authorized.

Most services have a common response pattern, generalizing it yields a very productive framework where adding support for new calls requires very little to no code. And you must never, ever expose to the user that you’re making SOAP requests and return any kind of raw SOAP object. Return domain-specific classes with attributes on success and raise exceptions otherwise.

Testing SOAP Requests


The tax_cloud gem uses VCR to test SOAP requests. It’s surprisingly easy: use a cassette (a YAML file), which records it the first time you make a request. Second time around the file contents are used and no HTTP requests are made. You can filter out sensitive keys in the configuration.
    require 'vcr'
     
    VCR.configure do |c|
      c.cassette_library_dir = 'test/cassettes'
      c.hook_into :webmock
      c.filter_sensitive_data('api-login-id')  { TaxCloud.configuration.api_login_id }
      c.filter_sensitive_data('api-key')       { TaxCloud.configuration.api_key }
      c.filter_sensitive_data('usps-username') { TaxCloud.configuration.usps_username }  
    end


    def test_ping_with_invalid_credentials
      assert_raise TaxCloud::Errors::ApiError do
       VCR.use_cassette('ping_with_invalid_credentials') do
        TaxCloud.client.ping
       end
      end
    end
     
    def test_ping_with_invalid_response
      e = assert_raise TaxCloud::Errors::UnexpectedSoapResponse do
       VCR.use_cassette('ping_with_invalid_response') do
        TaxCloud.client.ping
       end
      end
      assert_equal "Expected a value for `ping_result`.", e.problem
    end
     
    def test_ping
      VCR.use_cassette('ping') do
       response = TaxCloud.client.ping
       assert_equal "OK", response
      end
    end


You can see the rest of the tests here.

Finally


Let me know if you use some of these ideas, post your comments and suggestions and please help improve the tax_cloud gem on Github.

Published at DZone with permission of its author, Daniel Doubrovkine. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)