Applying monkey patches in Rails

ayushn21

Ayush Newatia

Posted on May 25, 2021

Applying monkey patches in Rails

Monkey patching is one of Ruby's most powerful features. It allows programmers to add methods to core classes which can result in some very elegant APIs. However it's quite easy to shoot yourself in the foot if you don't know what you're doing.

This post from Justin Weiss is a brilliant guide on how to monkey patch responsibly. However he doesn't describe how to actually include your monkey patches in a Rails app, so that's what I'm going to describe in this post.

Following Justin's advice, the implementation of all our monkey patches should go in the lib/core_extensions directory. So we might have some files like this:

lib/core_extensions/array.rb

module CoreExtensions
  module Array
    def to_set
      Set.new(self)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

lib/core_extensions/hash.rb

module CoreExtensions
  module Hash
    def keys_as_set
      Set.new(keys)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The lib/ directory in Rails is not autoloaded, so to apply these patches we need to run some code when our app boots. The best place to do this is to create a file called monkey_patches.rb under config/initializers/. All files in this directory are executed when Rails boots.

The contents of the file would look like:

# Require all Ruby files in the core_extensions directory
Dir[Rails.root.join('lib', 'core_extensions', '*.rb')].each { |f| require f }

# Apply the monkey patches
Array.include CoreExtensions::Array
Hash.include CoreExtensions::Hash
Enter fullscreen mode Exit fullscreen mode

This method works fine when we're patching Ruby's core classes, but if we want to patch classes in Rails frameworks such as ActiveStorage or ActionText, it's a bit more tricky as those classes may not be loaded as yet when the initializers are executed.

As you might expect from Rails, there's an elegant way to hook into the load process of those classes via ActiveSupport. So if we wanted to apply patches to ActiveStorage::Attachment and ActionText::RichText, we can include the following code in the monkey_patches.rb:

ActiveSupport.on_load(:action_text_rich_text) do
  ActionText::RichText.include CoreExtensions::ActionText::RichText
end

ActiveSupport.on_load(:active_storage_attachment) do
  ActiveStorage::Attachment.include CoreExtensions::ActiveStorage::Attachment
end
Enter fullscreen mode Exit fullscreen mode

The above code will apply our patches right after the relevant classes are loaded!

If you look at the source code for one of the above two classes, you'll see a line like this right at the bottom:

ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
Enter fullscreen mode Exit fullscreen mode

This is where the name of the hook we pass into the on_load method when applying your patch is defined. 

You can also run load hooks for your own app's classes in the same way to apply some configuration at boot time. A great example would be when using the adapter pattern to integrate with external services, but that's a topic for another blog post!

This post was originally published on my blog.

💖 💪 🙅 🚩
ayushn21
Ayush Newatia

Posted on May 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related