So I have taken the plunge and spent quite a bit of time playing with RoR (Ruby on Rails), in fact I decided to port my current projects serverside over to ruby to make the Ajax work easier and for the Experience. Along the way I had to write about 15 webservices to use for client server sync. Now if you don't know Ruby on Rails then the rest of this won't make much since so go of and learn a little rails and specifically ActionWebServices, then come back and you will think this is pretty cool :-) For the rest of you read on :-).
For security reasons Ruby on Rails doesn't support passing ActiveRecord objects to and from the client (not by default anyways). The fear is that someone will blindly pass in bad data, and the developer will just save it (without validation) therefore causing all kinds of issues. Now you can get around that by setting a parameter at the top of your ActionWebService::Base. But (at least for me) all that did was trade one problem (security) for another (exceptions being thrown because I don't use Integer ID's). I thought about it a bit and I realized I would need ActiveWebService::Struct proxies for all of my objects. I looked at my 20 tables and realized that while that would be a pain , it wouldn't be "so" bad.. But hand writing a Struct for each of my objects, then one by one writing a "to" and "from" converter just didn't sound fun. Not to mention that it wasn' a long term solution. I mean what if the next project has 300 tables?
Deep in thought considering the problem I decided what I really needed to do was harness what it means to be a dynamic language. I wanted a module or base class, that would take a ActionWebService::Struct and a ActiveRecord, and dynamically do the translation for me.. Put a ActiveRecord in and get a specific ActionWebService::Struct out, put a Stuct in and get the "right" ActiveRecord back. To that end I wrote WsBase
module WsBase
@basedOnType = Class
def map_from_class (type)
if (!type.base_class == (ActiveRecord::Base))
raise(ActionWebServiceError, "ActiveRecord model classes is expected")
end
@basedOnType = type
type.columns.each do |prop|
member prop.name, prop.type
end
end
def fill_base(me)
mydata = me.dup
#raise @returnval.to_yaml
hashVal = Hash.new
@basedOnType.columns.each do |attrib|
hashVal[attrib.name] = mydata[attrib.name]
end
returnval = @basedOnType.find(:first, :conditions => ["id = ?", mydata["id"]])
if (returnval == nil)
returnval = @basedOnType.new(hashVal)
returnval.id = mydata["id"]
else
hashVal.each do |field, val|
eval "returnval." + field + " = val"
end
end
return returnval
end
def fill_me(myRec,me)
myval = me.dup
myRec.attributes.each do |attrib,val|
eval "myval." + attrib + " = val"
end
myval.id = myRec.id
return myval
end
end
The idea behind this module is that I create a 3 line proxy class that inherits from ActionWebService::Struct. I then extend it with this module and then add the line map_from_class ActiveRecordClassName.. Thats it. If I want to populate the Struct I pass in my ActiveRecord, If I want to get back my ActiveRecord, I pass in my populated Struct. The class files look something like this
class WsContact < ActionWebService::Struct
extend WsBase
map_from_class Contact
end
And a "read method" in my Webservice controller would look like this
def get_contact(contactId)
wcontact = WsContact.new
arcontact = Contact.find(:first, :conditions => ["id = ?",contactId])
if (arcontact != nil)
returnval = WsContact.fill_me(arcontact, wcontact)
return returnval
end
return nil
end
and finally my write method would only need to be this
def save_contact(contact)
record = WsContact.fill_base(record)
if (record.save)
return "Good"
end
return "Bad"
end
Now isn't that elegent. And best of all maintance for my model doesn't mean I have to update these classes, they automaticly change because they take on the properties of their ActiveRecord :-).. I even made the code cleaner by making the write function take a param of type, so instead of having 15 write methods I have 1 write method that gets called 15 diffrent ways.. and the write method doesn't change, again taking advantage of rubys dynamic nature, you can call static methods of a parameter :-P..
Anyways, this all qualified as TOO cool not to share in my book.. If anyone has any updates or suggestions on how to write this dynamic code I am all ears, I am a Ruby Neophite so I am sure my fill_base and fill_me functions could be optimized or follow do a better job of following Ruby best practices.