Monkey Patching Models for Fun and Profit

Recently I came across an issue where I had several tables in my rails application that needed options. I really didn't want to just add options fields to each of them for obvious reasons (the options might change, etc). In the code of the project, the options were represented as a hash unique to each individual user ie: { :display_creator => true, :position => 1.1 }. So in order to abstract some of this away I decided to go with a polymorphic table in ActiveRecord, which works like so:

class Option < ActiveRecord::Base   belongsto :owner, :polymorphic => true end class User < ActiveRecord::Base   hasmany :options, :as => :owner end class Operation < ActiveRecord::Base   has_many :options, :as => :owner end

Pretty basic stuff, now every time I reference the options method from an individual user I get back a series of option objects associated with my users. If I reference the options method from my Operation object, I'll get back only the options associated with that particular operation. Rails does this by looking for two columns you must create when you create your model id and type, where name is specified in the :as parameter to the hasmany relationship. In my case it was :as => :owner, which meant my columns were :ownerid and :owner_type.

With this solution in place I was off to the races, however I noticed that I was having to convert all these options back into a hash (they were essentially being stored as a key value pair of strings in the db). Naturally this is an action that would be great to throw into the models, however I began to realize that I'd have to repeat this option object to hash method in each model if I wanted to store it there. And that's certainly not very DRY. Remembering some work I had done on the govkit library, I decided to use an acts_as reference condensing all the attributes of a model that are associated with having options down and creating a method that could add them to any individual model with a single call. (Note the Option class is unchanged)

config/initializers/activerecordaddons.rb module ActiveRecordAddons   def hasoptions     classeval do       hasmany :rawoptions, :as => :owner, :classname => "Option"       def options         opts = {}         rawoptions.each |opt|           opts[opt.key.to_sym] = converter(opt.value)         end         opts       end     private       def converter val         Integer(val)         rescue ArgumentError           Float(val)         rescue ArgumentError           val       end     end   end end ActiveRecord::Base.extend ActiveRecordAddons

You'll note that now when you call options, it's actually referencing the options method which parses out the rawoptions from the database and returns a hash. If I were doing this from an independant library, I would have packaged this all up in a gem and included the gem in my gemfile, however I'm only using this from my one project, so I instead wrote the code out and added it to an activerecordaddons.rb file in the config/initializers directory. This ensured that the method was monkey patched onto the ActiveRecord::Base parent before the models were instansiated. So now ActiveRecord::Base and all it's children have this hasoptions method available, which will add in all the necessary code to make the models able to store their options hashes. So now my models look like so:

class User < ActiveRecord::Base   hasoptions end class Operation < ActiveRecord::Base   hasoptions end

Nice and DRY, and now any changes I need to make to how these models deal with their options can be done from one place.

Hopefully someone else finds this helpful. Enjoy!



 

That's great for reading options. But how do you save them back to options.

 

Hmmm... I'd probably define a options= method to iterate through the hash and update/add the options to the object, so that you can maintain the nice User.first.options = { :key => "value" } format. Though given that the = operator is universally understood to set the value of something I'd probably also add an add_options method to just append options without deleting any or resetting the full object.

Something like this: https://gist.github.com/1158636