Warning: Missing argument 2 for wpdb::prepare(), called in /home/psq/nanorails.com/wp-content/themes/canvas/functions/admin-functions.php on line 947 and defined in /home/psq/nanorails.com/wp-includes/wp-db.php on line 1154
after_method | nanoRAILS

after_method

It started innocently enough. I was writing a Rails Plugin, and needed to call a Class Method on a Model. No problemo, right? In init.rb just call the method (To register a renderer in this case).


  Site.register_template_handler(".erubis", ErubisTemplate)

The method goes on and adds some date to a Class Variable. I try it and it worked…

… For the first request only :( After that, the data is gone from the class! What gives???

Well, there is a very convenient mechanism that reloads all your models, views and controllers so you don’t have to restart your app every time you make a change. Except here. Once the class gets unloaded, BAM! There goes the Class Variable and my carefully registered renderer.

First Solution

The code responsible for unloading and reloading all the models, views, and controllers is Dispatcher.reset_application!, so why not piggy back on reset_application! ? So using alias_method_chain, after a few iteration, here’s what I came up with:


  class << Dispatcher
    def register_erubis_template
      Site.register_template_handler(".erubis", ErubisTemplate)
    end
    def reset_application_with_erubis_registration!
      returning reset_application_without_erubis_registration! do
        register_erubis_template
      end
    end
    alias_method_chain :reset_application!, :erubis_registration
  end
  Dispatcher.register_erubis_template

Since reset_application! is not called till the second request, I had to also call the registration method once, which is why I created a register_erubis_template method.

It now worked. That was nice. But, then I started implementing a second plugin, which needed the same code. Not very DRY.

So I wrote down what ideally I wanted to end up with:


after_reset_application {
  Site.register_template_handler(".erubis", ErubisTemplate)
}

So next time I have the same problem, I can just reuse the same after_reset_application call with any other code I need to run after all the classes get unloaded.

Lots of aborted attempts

Foolishly, I thought it would be quite easy to transform what I had into something reusable. A little dose of Monkey Patching here and there, a bit of Meta Programming, and there you have it. Well, I did not keep all the many iterations I tried, here are just a few that did not work for one reason or an other (I did not try to find out why, just moved on to the next attempt)


  Dispatcher.alias_method_chain :reset_application!, #{feature}
  Dispatcher.class_eval do
    def reset_application_with_#{feature}!
      returning reset_application_without_#{feature}! do
        register_#{feature}
      end
    end
  end

def after_reset_application(feature, &block)
  patch = <<-end_eval
    class << Dispatcher
      def register_#{feature}
        proc
      end
      def reset_application_with_#{feature}!
        returning reset_application_without_#{feature}! do
          register_#{feature}
        end
      end
      alias_method_chain :reset_application!, #{feature}
    end
  end_eval
  eval patch#, &block, __FILE__, __LINE__
  Dispatcher.send "register_#{feature}"
end

Use the singleton-class, Luke! Use the singleton-class!

Then I retrieved a post from Ola Bini that I had read some months back: The ruby singleton class and I started to make some progress after I understood that to define methods, I needed to do it on the singleton class, not the class itself, because even though it did not fail, it did not accomplish much. And finally arrived at:

To retrieve that singleton class:


class << Dispatcher; self end

So now, I finally arrived at this code:


require 'dispatcher'

def after_reset_application(feature, &block)
  class << Dispatcher; self end.class_eval do
    define_method("register_#{feature}", &block)
    define_method("reset_application_with_#{feature}!") {
      returning Dispatcher.send("reset_application_without_#{feature}!") do
        Dispatcher.send("register_#{feature}")
      end
    }
    alias_method_chain :reset_application!, "#{feature}"
  end
  Dispatcher.send("register_#{feature}")
end

after_reset_application("erubis_registration") {
  Site.register_template_handler(".erubis", ErubisTemplate)
}

The key reasons why this works is that it is adding the methods to the singleton class, not the class itself. It is using send to be able to build the method names, and using alias_method_chain to be able to monkey patch reset_application! as many time as needed. I had to add a parameter to after_reset_application for the same reason, so it can be called more than once. And it uses define_method because you can give it a block, plus you can define the method name from variables.

All good, except that it is still not very DRY. What if I want to use the same technique on a different class? A different method?

The final version (for now?)

This one was easy, just add a few parameters, deal safely with ?, ! and = and there you go


def after_method(klass, target, feature, &block)
  # Strip out punctuation on predicates or bang methods since
  # e.g. target?_without_feature is not a valid method name.
  aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
  class << klass; self end.class_eval do
    define_method("register_#{feature}", &block)
    define_method("#{aliased_target}_with_#{feature}#{punctuation}") {
      returning klass.send("#{aliased_target}_without_#{feature}#{punctuation}") do
        klass.send("register_#{feature}")
      end
    }
    alias_method_chain target, "#{feature}"
  end
  klass.send("register_#{feature}")
end

Then, to implement after_reset_application:


require 'dispatcher'

def after_reset_application(feature, &block)
  after_method(Dispatcher, :reset_application!, feature, &block)
end

And to use:


after_reset_application("erubis_registration") {
  Site.register_template_handler(".erubis", ErubisTemplate)
}

Phew! The block gets called once the first time, and every time the classes need to be reloaded.

Tags: , , , ,

Comments are closed.