Ruby Metaprogramming part 2

Last time I discussed Ruby and metaprogramming, I was trying to stay DRY (Don’t Repeat Yourself) while coding some very similar-looking methods. The solution, then was to use class_eval to dynamically add methods into the current class, the way attr_accessor and it’s peers already do.

Today I was having a similar, yet more difficult problem: the architecture of my app includes two controllers: IncomeController and ExpenseController, two identical twins, each saving, updating and listing it’s associated models (Income/Expense), where Income has_many income_attributes and.. you guessed it, Expense has_many expense_attributes. And the list can go on, since they have some more relationships, all of them being identical but for the names and associations.

I know, perhaps such thing might have been avoided if, in the first place, we would have used single-table inheritance and such. But for various reasons we didn’t, and I was getting tired of keeping in sync all the changes in Income* to Expense* and vice-versa.

So.. what could the solution be? Namely, I wanted both IncomeController and ExpenseController to share the same codebase, the only difference being the names; I wanted a… replace_all to be run on the fly.

Luckily for me, I remembered Camping, a Ruby Microframework for web apps(kinda like Rails, but more simplistic), which uses some intense metaprogramming-kung-fu to keep its code to less than 4kb. For instance, in the homepage example, we see that

A skeletal Camping blog could look like this:
require ‘camping’
Camping.goes :Blog

The .goes method returns a class duplicate of the Camping one, where all occurences of Camping have been changed with Blog.

The code I use for my metaprogramming needs is quite similar to the one of Camping, adding just a bit more readability and a couple of changes to make things work in my app:

I created a IncomeExpenseController class in income_expense_controller.rb, containing all the common functionality; I made sure to replace_all occurences of Income by IncomeExpense; same for the lowecase version.

Then, in income_controller.rb I have only this:
IncomeExpenseController.goes :Income
Similar, in expense_controller.rb I have only this:
IncomeExpenseController.goes :Expense

In the income_expense_controller.rb file, within the body for IncomeExpenseController class, I added this:


#start metaprogramming
S = IO.read(__FILE__) rescue nil
NAME = "IncomeExpense"
class << self
  def goes(m)
    myS = S.gsub(Regexp.new(NAME), m.to_s)
    myT = myS.gsub(Regexp.new(NAME.underscore), m.to_s.downcase)
    eval myT, TOPLEVEL_BINDING
  end
end
# end metaprogramming

I actually tried to put this #metaprogramming part in a separate module, but for some unknown reason it wouldn’t work that way. If anyone succeeds in making it work within a separate module, I’d like to know how.

Note that the String.underscore method is part of ActiveSupport, so it’ll work on Rails. Also note that I added this metaprogramming DRY fragment in quite a lot of my files: the Income/Expense models, the IncomeAttribute/ExpenseAttribute models and many more.
I love it!

a

Ruby Metaprogramming part 2


Similar Posts:

0 Comments

Leave a Reply

Your email address will not be published.