Just Enough Developed Infrastructure

Build a SOAP Service with Rails 2.x


In the days of Rails 1.x, building a SOAP service in Rails was easy. There was the actionwebservice gem that enabled SOAP services, similar to what we know as ActionResource today.
Be it good or bad, SOAP eventually got replaced by REST within the Rails world, leaving the net scattered with examples on how to build a SOAP service with Rails 1.x but not much for 2.x
The first thing I tried (but did not work):
Inspired by http://www.devarticles.com/c/a/Ruby-on-Rails/Web-Forms-and-Ruby-on-Rails/2/

$ gem install actionwebservice

This installed ok, bringing all the older versions of action series gems along with it.


$ ./script/generate web_service ...

It did not have the generator, so much for this solution


The solution: enter datanoise actionwebservice:
The guys from datanoise, brought it back alive with a version that works in rails 2.x again.
Details on how to use it, can be found on http://www.datanoise.com/articles/2008/7/2/actionwebservice-is-back
I'll redo the example done in the rails 1.x article to the datanoise style

$ gem install datanoise-actionwebservice --source http://gems.github.com
./script/generate
....
Installed Generators
  Rubygems: web_service
  Builtin: controller, helper, integration_test, mailer, metal, migration, model, observer, performance_test, plugin, resource, scaffold, session_migration
....

$ ./script/generate web_service Item create app/services/ exists app/controllers/ exists test/functional/ create app/services/item_api.rb create app/controllers/item_controller.rb create test/functional/item_api_test.rb
As you can see, it puts its files in /app/services instead of app/api .
Next step is to add the gem to the conf/environment.rb so that is knows the correct actionwebservice gem

     config.gem 'datanoise-actionwebservice', :lib => 'actionwebservice'
Modify the file app/controllers/item_controller.rb

class ItemController < ApplicationController
  wsdl_service_name 'Item'
  web_service_api ItemApi
  web_service_scaffold :invocation if Rails.env == 'development'

def add(name, value) Item.create(:name => name, :value => value).id end
def edit(id, name, value) Item.find(id).update_attributes(:name => name, :value => value) end
def fetch(id) @item1=Item.find(id) Item.new(:id => "{@item1.id}", :value => "#{@item1.value}", :name => "#{@item1.name}") end end
Modify the file app/controllers/item_api.rb

class ItemApi < ActionWebService::API::Base
    api_method :add, :expects => [:string, :string], :returns => [:int]
    api_method :edit, :expects => [:int, :string, :string], :returns => [:bool]
    api_method :fetch, :expects => [:int], :returns => [Item]
end
Ok, let's start the service know, and see if we can read the wsdl file

$ ./script/server
$ curl http://localhost:3000/item/wsdl
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="Item" xmlns:typens="urn:ActionWebService" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="urn:ActionWebService" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns="http://schemas.xmlsoap.org/wsdl/"&gt;
  <types>
    <xsd:schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:ActionWebService">
      <xsd:complexType name="Item">
        <xsd:all>
          <xsd:element name="id" type="xsd:int"/>
          <xsd:element name="name" type="xsd:string"/>
          <xsd:element name="value" type="xsd:string"/>
          <xsd:element name="created_at" type="xsd:dateTime"/>
          <xsd:element name="updated_at" type="xsd:dateTime"/>
        </xsd:all>
      </xsd:complexType>
    </xsd:schema>
  </types>
 .......

$ curl http://localhost:3000/item/invocation ...some..nice..readable..html...
So far so good, let's build a Soap Client to use the service via ruby. Create a file test.rb

require 'soap/wsdlDriver'
wsdl = "http://localhost:3000/item/service.wsdl"
item_server = SOAP::WSDLDriverFactory.new(wsdl).create_rpc_driver
item_id = item_server.add('foo', 'bar')
if item_server.edit(item_id, 'John', 'Doe')
   puts 'Hey, it worked!'
else
   puts 'Back to the drawing board...'
end

Hey, it worked!

item = item_server.fetch(item_id) item.class # => SOAP::Mapping::Object item.name # => "John" item.value # => "Doe"

If we run this, it will spit out the error “Cannot Map {objType} to SOAP/OM”.
It seems to be a problem with the way the Arrays are handled. I'm unsure how to describe the real issue, but here are some links about the subject.

The solution was to use another notation assign the values to a new Item using the #{@item1.value} notation. So if we change the function fetch:

  def fetch(id)
    @item1=Item.find(id)
    Item.new(:id => "{@item1.id}", :value => "#{@item1.value}", :name => "#{@item1.name}")
  end
The example now works again. So far, Soap for Rails seems to be working ok.
P.S. If you are consuming it with axis2, chances are that it uses 'Transfer-Encoding: chuncked'. Actionwebservice doesn't seem to handle this ok.