Evil Twin Plugin

Seriously, I think I have something against Rails’ lib directory. We jumped from keeping gems in lib to vendor/gems back in March. Then we jumped from keeping generic Rake tasks in lib/tasks to Sake. Now we’re gonna jump again.

Hacking Plugins

It’s really not that big of a deal, and pretty common—you want to change the behavior of some existing plugin. Maybe you Piston it and commit your changes. Sure. But maybe you just want to leave the original code alone.

A classic approach has been to stick some kind of hack in the lib directory. Issues abound, for sure. First: the load order. Who gets loaded first? Who reloads and who doesn’t? Second: location. You’ve got one bit of code messing with another bit of code in a totally separate location. Third: testing. Are you testing it? Maybe.

None of these things are deal breakers, but we can certainly address them. And we will.

The Evil Twin Plugin

Evil Twin

Here’s the simple solution: create a plugin called whatever_hacks, where whatever is the name of the plugin you’re hacking. That’s it. An evil twin, if you will.

Adding the _hacks suffix ensures it will always be loaded after the target plugin (assuming you haven’t messed with the default plugin load order—alphabetical). Keeping it right next to the target plugin also ensures anyone who peers into vendor/plugins will instantly know tomfoolery is afoot.

You can now build out a tested, hack happy plugin. Or, y’know, just stick it all in init.rb. With caution.

Caution: init.rb

Caution: init.rb does not always do what you expect it to do. It’s loaded in the context of Rails::Plugin in 2.0 and Rails::Initializer in 1.2.5, not Object. Come again? Like this: re-opening existing classes isn’t as straightforward as elsewhere.

=> init.rb

class Hash
end
puts Hash.inspect

Guess what that prints. Ready?

$ ./script/runner 
Rails::Plugin::Hash

That’s right—we didn’t re-open Hash, we created a new Rails::Plugin::Hash class. Any methods we add in there won’t be added to Hash proper.

If we want to grab a real class and stuff some methods in it, we need to use class_eval or module_eval:

=> init.rb

Hash.class_eval do
  def duck_punched?
    true
  end
end  

puts({}.duck_punched?)

As expected:

$ ./script/runner 
true

Doing it this way (class_eval) forces a constant lookup, making Ruby happily run up the chain and find the class or module in question.

attachment_fu_cropper

Okay, time for a real example. I wanted to change attachment_fu’s ImageScienceProcessor to crop thumbnails before resizing them. As this is a hack I use on all my apps, I also want to keep it out of my models. Hence, attachment_fu_hacks.

=> vendor/plugins/attachment_fu_hacks/init.rb

klass = Technoweenie::AttachmentFu::Processors::ImageScienceProcessor
klass.module_eval do
  ##
  # Hacked to use image_science's #cropped_thumbnail method
  def resize_image(img, size)
    # create a dummy temp file to write to
    filename.sub! /gif$/, 'png'
    self.temp_path = write_to_temp_file(filename)
    grab_dimensions = lambda do |img|
      self.width  = img.width  if respond_to?(:width)
      self.height = img.height if respond_to?(:height)
      img.save temp_path
      callback_with_args :after_resize, img
    end

    size = size.first if size.is_a?(Array) && size.length == 1
    if size.is_a?(Fixnum) || 
        (size.is_a?(Array) && size.first.is_a?(Fixnum))
      if size.is_a?(Fixnum)
        img.cropped_thumbnail(size, &grab_dimensions)
      else
        img.cropped_thumbnail(size.first, &grab_dimensions)
      end
    else 
      new_size = [img.width, img.height].dim size.to_s

      img.cropped_thumbnail(new_size.first, &grab_dimensions)
    end
  end
end 

Works like a charm.

When heavysixer wanted to hack acts_as_taggable, he took the same approach: http://pastie.caboo.se/119904. Feel free to follow suit.