Skip to content
This repository

This guide gives recommendations for how to work with Handsoap. You don’t need to follow these conventions at all – This is just the workflow that I have found useful.

Workflow

  1. You need a WSDL for the service you want to consume.
  2. Figure out the url for the endpoint, as well as the protocol version. Put this in a config file.
  3. To find the endpoint, look inside the wsdl, for <soap:address location="..">
  4. Create a service class. Add endpoints and protocol. Alias needed namespace(s).
  5. To find the namespace(s), look in the samples from soapUI. It will be imported as v1
  6. Note that you can now use the provided generator to skip this step.
  7. Open the wsdl in soapUI
  8. In soapUI, find a sample request for the method you want to use. Copy+paste the body-part.
  9. Create a method in your service class (Use ruby naming convention)
  10. Write Ruby-code (using XmlMason) to generate a request that is similar to the example from soapUI. (In your copy+paste buffer)
  11. Write Ruby-code to parse the response (an XML-document) into Ruby data types.
  12. Write an integration test to verify that your method works as expected. You can use soapUI to generate a mock-service

Repeat point 5..9 for each method that you need to use.
Between each iteration, you should refactor shared code into helper functions.

Configuration

If you use Rails, you should put the endpoint in a constant in the environment file. That way, you can have different endpoints for test/development/production/etc.

If you don’t use Rails, it’s still a good idea to move this information to a config file.

The configuration could look like this:

# wsdl: http://example.org/ws/service?WSDL
EXAMPLE_SERVICE_ENDPOINT = {
  :uri => 'http://example.org/ws/service',
  :version => 2
}

If you use Rails, you will need to load the gem from the config/environment.rb file, using:

config.gem 'troelskn-handsoap', :lib => 'handsoap', :source => "http://gems.github.com"

If you use the standard development environment of Rails, you may run into troubles with cached classes. Add the following line to the initializer:

ActiveSupport::Dependencies.explicitly_unloadable_constants << 'Handsoap::Service'

Generator

From version 0.2.0 Handsoap sports a generator, that creates the service class + an integration test case. This is just a rough starting point for your service – You still have to fill out the actual mappings to/from xml, but at least it saves your some copy-pasting from this guide.

To use the generator, create a Rails project and run the script script/generate handsoap, giving the url of the WSDL.

Service class

Put your service in a file under app/models. You should extend Handsoap::Service.

You need to provide the endpoint and the SOAP version (1.1 or 1.2). If in doubt, use version 2.

A service usually has a namespace for describing the message-body (RPC/Literal style). You should set this in the on_create_document handler. Likewise, the response returned from the server will contain elements that typically are defined in a single namespace relevant to the service. You can register this in the handler on_response_document.

A typical service looks like the following:

# -*- coding: utf-8 -*-
require 'handsoap'

class Example::FooService < Handsoap::Service
  endpoint EXAMPLE_SERVICE_ENDPOINT
  def on_create_document(doc)
    # register namespaces for the request
    doc.alias 'tns', "http://example.org/ws/spec"
  end
  def on_response_document(doc)
    # register namespaces for the response
    doc.add_namespace 'ns', 'http://example.org/ws/spec'
  end

  # public methods
  # todo

  private
  # helpers
  # todo
end

The above would go in the file app/models/example/foo_service.rb

Integration tests

Since you’re writing mappings manually, it’s a good idea to write tests that verify that the service works. If you use standard Rails with Test::Unit, you should put these in an integration-test.

For the sample service above, you would create a file in test/integration/example/foo_service.rb, with the following content:

# -*- coding: utf-8 -*-
require 'test_helper'

# Example::FooService.logger = $stdout

class Example::FooServiceTest < Test::Unit::TestCase
  def test_update_icon
    icon = { :href => 'http://www.example.com/icon.jpg', :type => 'image/jpeg' }
    result = Example::FooService.update_icon!(icon)
    assert_equal icon.type, result.type
  end
end

Note the commented-out line. If you set a logger on the service-class, you can see exactly which XML goes forth and back, which is very useful for debugging.

Methods

You should use Ruby naming-conventions for methods names. If the method has side-effects, you should postfix it with an exclamation.
Repeat code inside the invoke-block, should be refactored out to builders, and the response should be parsed with a parser.

def update_icon!(icon)
  response = invoke("tns:UpdateIcon") do |message|
    build_icon!(message, icon)
  end
  parse_icon(response/"//icon").first)
end

Helpers

You’ll end up with two kinds of helpers; Ruby→XML transformers (aka. builders) and XML→Ruby transformers (aka. parsers).
It’s recommended that you stick to the following style/naming scheme:

# icon -> xml
def build_icon!(message, icon)
  message.add "icon" do |i|
    i.set_attr "href", icon[:href]
    i.set_attr "type", icon[:type]
  end
end
# xml -> icon
def parse_icon(node)
  { :href => (node/"@href").to_s, :type => (node/"@type").to_s }
end

or, if you prefer, you can use a class to represent entities:

# icon -> xml
def build_icon!(message, icon)
  message.add "icon" do |i|
    i.set_attr "href", icon.href
    i.set_attr "type", icon.type
  end
end
# xml -> icon
def parse_icon(node)
  Icon.new :href => (node/"@href").to_s,
           :type => (node/"@type").to_s
end
Something went wrong with that request. Please try again.