For a project, I'm currently implementing a SOAP API in Rails. The Rails application will function as a SOAP server and I'm using the datanoise activewebservice plugin for this. Implementing a SOAP API is pretty easy with this, however, I ran into some problems with returning complete model objects.
I have a model called Customer that I want to expose via my API. After setting up the API controller, I specified the following in /app/services/customer_api.rb:
class CustomerAPI < ActionWebService::API::Base api_method :get, :expects => [:int], :returns => [Customer] end
Implemented with:
def get(id) return Customer.find(id) end
Requesting the WSDL nicely gives the definition of Customer, but when making a request for a specific customer errored with the message "Cannot map Customer to SOAP/OM.". It appears that boolean values (in Ruby presented as true/false) and emtpy values (presented in Ruby as nil), were the problem. SOAP wants them as "true", "false" and "".
I solved this by extending my models with a to_struct() method that returns all attributes in a Struct. I extended both ActiveRecord::Base instances to get the instance method to_struct() inside my models and I extended ActiveRecord::Base class so it would return a Struct. The latter is needed, since the SOAP plugin wants to know the attributes and types of the Struct, without actually creating one. Here's what I did:
To extend ActiveRecord::Base with class methods create a module and put the following in your /config/environment.rb:
require 'model_extensions' ActiveRecord::Base.send(:extend, ModelExtensions)
Then in the module:
module ModelExtensions
def struct
# Check if the DataStruct is already defined
begin
eval("#{self}::DataStruct")
rescue
# Define the DataStruct class and fill it with
# members that resemble the model
class_eval <<-EOF
class DataStruct < ActionWebService::Struct
#{self}.columns.map{|c| member c.name.to_s, c.type.to_s}
end
EOF
end
return eval("#{self}::DataStruct")
end
end
Now to create a DataStruct in a model, we define the following:
module ActiveRecord
class Base
def to_struct
# Get a DataStruct and instantiate
struct = self.class.struct.new
# Set the attributes after clean-up
self.attributes.each do |k,v|
v = (v.nil?) ? "" : ((v == true) ? "true" : (v == false) ? "false" : v)
struct.send("#{k}=", v)
end
return struct
end
end
end
To finish off, we change the API implementation a bit to the following:
class CustomerAPI < ActionWebService::API::Base api_method :get, :expects => [:int], :returns => [Customer.struct] end
Implemented with:
def get(id) return Customer.find(id).to_struct end
Looks exactly like something I could use – But …
I’ve added your code to /config/environment.rb
(took some tries before I found out it had to be in the very bottom of that
model_extensions.rb placed in /lib … and works.
But I can’t find out where to place the code that defines the to_struct ???
Can you please help? Until now I have resorted to making a ActionWebService::Struct for each of my models, so this can really save me some coding!
Thanks in advance!
What I did was putting my ActiveRecord::Base extensions (the to_struct and I have some others) in the model_extensions.rb in /lib. Just below the ModelExtensions module. Looks like this:
module ModelExtensions
…
end
module ActiveRecord
class Base
…
end
end
As long as these things are loaded somewhere.
Wouter: Thank you! It works now !!!
Should have thought of that actually – just got a little confused since the code was split in two parts, so I assumed they had to be in two different places
Now I know what to spend my easter-coding with