XPlayer

A blog on my daily efforts to be a better developer, and keep improving every day.

Dynamically Add Data Accessor Methods on "Static" Rails Data Model

| Comments

An useful metaprogramming spell I recently played with is the Module#define_method(), which dynamically adds an instance method to the class on which is called.   I found it particularly useful to add data accessor methods on “static” Rails data model: suppose I’m working an e-commerce Rails webapp, and I have a Country model which maps the countries suitable for shipping, or a PaymentType model which represents all the possible payment types. For these kind of models (and tables), which are typically static (they don’t change often), you often have to access specific values, say Country.italy or PaymentType.credit_card. In these cases, defining dynamically an accessor method may be useful and more clear than always perform a find_by_name("my value"). So, for example, I open up my country.rb model class and add these lines [sourcecode language=”ruby”] class << self Country.all.each do |each_country| define_method(each_country.name.downcase.gsub(‘.’, ”).gsub(’ ‘, ‘_’)) do Country.find_by_iso_code(each_country.iso_code) end end end [/sourcecode] And then opening the Rails console I will be able to type
1.8.7@epistore > Country.sri_lanka
# {
                :id => 59,
              :zone => "U9",
           :enabled => true,
        :created_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
        :updated_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
          :iso_code => "LK",
    :country_set_id => nil
}
Just a note: as I said, Module#define_method() will add an instance method on the class. To add a class method, which is what I want, we have to use a different approach, using the class << self syntax to add a singleton method in the receiver. I may also add a query method on each Country instance to check that country against another country (for example, I may ask my_country.italy?)
  Country.all.each do |each_country|
    define_method(each_country.name.downcase.gsub('.', '').gsub(' ', '_').concat('?')) do
      has_iso_code? each_country.iso_code
    end
  end
And then, after issuing a reload! command in the Rails console, I may type:
1.8.7@epistore > Country.usa.usa?
true
1.8.7@epistore > Country.usa.italy?
false
1.8.7@epistore > Country.usa.south_korea?
false
1.8.7@epistore > Country.south_korea.south_korea?
true
Depending on the kind of Rails app you have, these may be a useful tip.