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
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.rbclass 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.rbHash.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.rbklass = 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.
Ah, neat tip. I like that putting “_hacks” at the end ensures it loads after the real plugin, load order being alphabetical and so on. I hadn’t thought of that!
Hilarious. As I was reading through this, I was thinking about the patch I did for cropping with Attachment_fu. Creepy.
This is sweet, I’ll try to factor out my patch using this method. I’ve never been a big fan of patching plugins directly, especially if you need to reuse what you did to it. This keeps it dry and distributable. Pretty slick.
im hacking our acts_as_taggable_ext, and this flies into my rss reader. cue twilight zone music.
We have a separate RAILS_ROOT/plugins directory for our “own” plugins (and pluginized code, to keep lib clean).
We also have a “snippets” plugin that mimics the ActiveSupport Railties folders, with names for each object or library we are patching, and a simple init.rb that loads all files inside lib:
Dir[File.dirname(FILE) + ”/lib/*/.rb”].each { |file| require(file) }}
There we store dozens of patches and little tweaks to ruby, rails and other plugins.
Having a separate whatever_hacks plugins makes sense if the changes are significant, but I rather have a single plugin for trivial snippets.
Instead of the whole class_eval business, can’t you just do:
class ::Hash def foo; ‘bar’; end end
::Hash? Gross!
“Gross, LOL :)
Remember when I wrote” in will_paginate? One of you guys couldn’t stand it :D
I said remember when I wrote ::Hash
Textile sucks, seriously
Textile? Gross!
LOL
The issues with init.rb are due to its weird implementation within Rails involving eval. I recommend running nothing but requires from within init.rb to make everything nice and clean—required files work as expected with no fallout that I’m aware of from the eval.
A little off topic, but how do you use your Attachment_fu hack so that you can get square (cropped) thumbnails?
Jim, all you have to do is put that code in the attachment_fu_hack dir as init.rb. When I tried it I got ‘undefined method `dim’ for Array’ error. I haven’t looked too closely but he probably just method that attachment_fu defines in Array. Chris may want to correct me on that though.
I love that you give these old conventions cool names. I made the original commit to rails that orders the plugins before loading them to make this kind of thing possible. Then, someone made a hacked version of acts_as_versioned that worked with associated models or something. Cool, hacky shit.
I was using lib/duck_punches, but this makes much more sense.
Jim, just replace one line, and it’ll work:
new_size = [img.width, img.height] / size.to_s
Cheers.
So this technique totally overwrites whatever method you’re extending?
For example, you hack resize_image above.
You have have to implement this whole method, you can just edit a few args and call super(img, new_size) and have the original resize_image finish.
Would this be correct?
Here’s a version that uses RMagick and also supports desaturation:
(I hope I get the textile
Annnd…I got the textile tags wrong. Maybe you guys can fix it up for me.
Chime in.