Monday, February 23, 2009

Playing with SOAP in Ruby on Rails using WSDL Driver Factory

-First, a little background on why I am using RoR and SOAP.
When I helped design and build an inventory and booking system circa 2007, my role was basically systems/business analyst. I also provided the impetus that we build the system using tools beyond simple "procedural" PHP/javascript, and that we look for a web application technology platform which would allow us to rapidly build a system that stored everything in a relational database.

Back in 2001, I wondered why I had to write SQL queries for web applications when I knew that if I was working with a well-designed database, there should be techniques to generate queries or even better... a developer could write objects that were basically entities from an ERD and actors from a UML and these objects knew which other objects were related to it and had methods to access all the information without having to write any SQL or be intimately aware of the relationships between tables.

So for our inventory and booking system, our developer found the Ruby on Rails (RoR) framework, which implements MVC architecture. This framework fit my requirements that we don't have to be writing lots of SQL and that we should use an agile method of software development. Eventually, our inventory and booking system was built with RoR and AJAX technologies but my role in its creation did not involve any coding.

Today, I need to build a system to integrate two applications, one that exposes a SOAP API and one that provides decent docoumentation to directly access the database.
-End Background

As a complete newbie to writing code for SOAP and for Ruby on Rails, I have encountered many challenges and tackled problems that an experienced RoR developer would have solved readily. It doesn't help that programming is merely a hobby of mine by necessity, and that I haven't written any serious code in years.

By the end of this post some of the most valuable websites available for someone trying to use RoR to consume SOAP services will be linked and referenced.

Starting with the WSDL Driver Factory standard library by using Ryan Heath's Consuming SOAP services in Ruby example. And similar examples.

The goal was to use his code in a model-view-controller architecture and being a novice, I had a lot of difficulty. I ended up with the following proof of concept; with minimal coding, RoR can consume a simple SOAP webservice. The specific webservice consumed in this example is one that calculates the amortized payment amount per period given the required parameters. I found this webservice on www.xmethods.net, and the developer appears to have implemented it in ColdFusion. Notice in this example, the class Amortization extends ActiveRecord, so the database is used to store persistent data, which may be useful if retaining a history of request inputs and response outputs from consuming a SOAP service is important.

This is the model file: amortization.rb

require 'soap/wsdlDriver'

class Amortization < ActiveRecord::Base

def amortization
@amortization ||= wsdl.create_rpc_driver.calculate principal, interest, num_payments
end

private
def wsdl
SOAP::WSDLDriverFactory.new(url)
end
end

This is the controller file: amortizations_controller.rb

class AmortizationsController < ApplicationController

def new
@amortization = Amortization.new :url => 'http://www.kylehayes.info/webservices/AmortizationCalculator.cfc?wsdl'
end

def create
@amortization = Amortization.new params[:amortization]
if @amortization.save
redirect_to @amortization
# redirect_to :action => 'index'
else
render :action => 'new'
end
end

def show
@amortization = Amortization.find params[:id]
end

def index
@amortizations = Amortization.find(:all)
end

def delete
@amortizations = Amortization.find(params[:id])
@amortizations.destroy
redirect_to :action => 'index'
end

end

Below are the views in the views/amortizations folder.

Beginning with new.html.erb


<h1>Consume a SOAP Web Service</h1>

<% form_tag :action => 'create' do %>

<p>
<label for="amortization_WSDL_URL">Amortization WSDL URL</label><br/>
<%= text_field 'amortization', 'url', :value => @amortization.url %>
</p>
<p>
<label for="amortization_principal">Principal Amount</label><br/>
<%= text_field 'amortization', 'principal' %>
</p>
<p>
<label for="amortization_interest">Interest (not in percent)</label><br/>
<%= text_field 'amortization', 'interest' %>
</p>
<p>
<label for="amortization_num_payments">Number of Payments</label><br/>
<%= text_field 'amortization', 'num_payments' %>
</p>
<p>
<%= submit_tag "Store inputs to Amortization calculator" %>
</p>
<% end %>

<%= link_to 'Back', {:action => 'index'}%>

show.html.erb

<p>
<b>SOAP Web Service Response:</b> $
<%=h @amortization.amortize %>
</p>

index.html.erb

<h1>Listing services</h1>

<table>
<tr>
<th>Amortization per period - click URL to calculate</th>
<th>Principal Amount</th>
<th>Interest Rate (not in percent)</th>
<th>Number of Payment periods</th>
</tr>

<% if @amortizations.blank? %>
<p>no amortizations in system</p>
<% else %>

<% @amortizations.each do |a| %>
<tr>
<td><%= link_to a.url, a -%></td>
<td><%=h a.principal -%></td>
<td><%=h a.interest -%></td>
<td><%= link_to a.num_payments, {:action => 'show', :id => a.id} -%></td>
<td><%= link_to 'Delete', {:action => 'delete', :id => a.id}, :confirm => 'Are you sure?' %></td>
</tr>
<% end %>
<% end %>
</table>

<br />

<%= link_to 'New amortization', {:action => 'new'}%>

You will need to add the following to the routes file found in config/routes.rb.

map.resources :amortizations

Also, create a migration for creating amortizations table in the database using the file below and run rake db:migrate

class CreateAmortizations < ActiveRecord::Migration
def self.up
create_table :amortizations do |t|
t.column :url, :string
t.column :principal, :decimal
t.column :interest, :decimal
t.column :num_payments, :decimal
t.timestamps
end
end

def self.down
drop_table :amortizations
end
end


In the next code sample, the data is not persistent and Amortization is not a subclass of Active Record. This could be useful, for example, when calling authentication methods of a webservice where you would not want to store the username/password. Thanks to the people in the Ruby on Rails IRC channel, the first method only needed minor modifications. Check out the channel #rubyonrails on irc.freenode.net.

The model, amort.rb

require 'soap/wsdlDriver'

class Amort
attr_accessor :url, :principal, :interest, :num_payments

def initialize(options = {} )
self.url = options[:url]
self.principal = options[:principal]
self.interest = options[:interest]
self.num_payments = options[:num_payments]
end

def data
%w(url principal interest num_payments).inject({}) do |acc, method|
acc.merge! method.to_sym => send(method)
end
end

def print_response
@soap = wsdl.create_rpc_driver
response = @soap.calculate(principal, interest, num_payments)
return response
end

private
def wsdl
SOAP::WSDLDriverFactory.new(url)
end
end

The controller, amorts_controller.rb

class AmortsController < ApplicationController

def new
@amort = Amort.new :url => 'http://www.kylehayes.info/webservices/AmortizationCalculator.cfc?wsdl'
end

def create
@amort = Amort.new params[:amort]
redirect_to url_for(:action => :show, :amort => @amort.data)
end

def show
@amort = Amort.new params[:amort]
end

end

new.html.erb

<h1>Consume a Web Service</h1>

<%= form_tag :action => 'create' %>
<p>
<label for="amort_WSDL_URL">Amortization WSDL URL</label><br/>
<%= text_field 'amort', 'url', :value => @amort.url %>
</p>
<p>
<label for="amort_principal">Principal Amount</label><br/>
<%= text_field 'amort', 'principal' %>
</p>
<p>
<label for="amort_interest">Interest (not in percent)</label><br/>
<%= text_field 'amort', 'interest' %>
</p>
<p>
<label for="amort_num_payments">Number of Payments</label><br/>
<%= text_field 'amort', 'num_payments' %>
</p>
<p>
<%= submit_tag "Create" %>
</p>

<%= link_to 'Back', amorts_path %>

show.html.erb

<p>
<b>The amortized payment per period is:</b>
<%=h @amort.print_response %>
</p>


<%= link_to 'Edit', edit_amort_path(@amort) %> |
<%= link_to 'Back', amorts_path %>

Don't forget to add a RESTful route for amorts into the routes.rb file. There is extra code in this sample, ie. the create method for the controller is not necessary and could be bypassed without a loss of functionality simply by changing the :action => 'create' in the new template to :action => 'show'

Other resources:
Another example
Ruby Standard Library Documentation (RDoc)
The full source of SOAP4R with examples is available at the "trac"-ing site

IRC:
If you want to use a controller method in a view, in the controller you need to specify helper_method :use_web_service, then you can call use_web_service in your views directly. That won't pass in a params hash, though, so you're better off creating a method that takes an array of arguments, and calling that from your use_web_service action and your views.

[code]Try at refactoring... http://pastie.org/398113[/code]
blog comments powered by Disqus