DEV Community

Lucian Ghinda
Lucian Ghinda

Posted on • Originally published at allaboutcoding.ghinda.com on

How to make sure you review your monkey patch when updating Ruby gems

When I installed Writebook for booklet.goodenoughtesting.com, I monkey-patched some parts of it because I wanted to remove the creation of the first book. Later, I added some default meta tags. You can read about my approach in the article Overriding Methods in Ruby on Rails: A No-Code-Editing Approach. You might monkey patch a gem or a part of Rails for different reasons.

I agree that monkey patching should be the last approach and used carefully. One of the most critical issues is that the code you create depends on the structure of the code you are monkey patching, thus voluntarily violating the contract that the gem's author offered.

I see monkey patching as a breach of contract with the author of a gem. In rare cases when this is necessary, the fix should be on the side that breached the contract. If you go down this route, then every time the gem is updated, you have to review the code of the gem and test your monkey patch to see if everything works as you expect it.

How to automate the check of patched Gem version

Add the following code at the beginning of your monkey patch file.

# This is an example about ActiveSupport

if Rails.env.local? || Rails.env.ci?
  if ActiveSupport.version > Gem::Version.new('8.0.0')
    raise "\n\nReview this monkey patch after upgrading \n" \
          "Path: #{__FILE__}"
  end
end
Enter fullscreen mode Exit fullscreen mode

You can do the same if you have an initializer and you temporary added some settings that you know you want to remove or review after doing an update.

I found this approach in various projects Ive worked on over the past couple of years. It can take different forms: sometimes only in CI, sometimes in a specific CI that checks compatibility, sometimes in tests, and other variations.

In case the gem does not define the version as an Gem::Version object and it might define it like this:

module MyGem
  VERSION = '1.0.1'
end
Enter fullscreen mode Exit fullscreen mode

Then you should create the comparison in the following way:

if Rails.env.local? || Rails.env.ci?
- if MyGem::Version > Gem::Version.new('8.0.0')
+  if Gem::Version.new(MyGem::VERSION) > Gem::Version.new('1.0.1')
    raise "\n\nReview this monkey patch after upgrading \n" \
          "Path: #{__FILE__}"
  end
end
Enter fullscreen mode Exit fullscreen mode

You should always compare with Gem::Version object because it defines the method <=> in a way that takes into consideration major, minor and patch version. Here is how that method currently looks like:

# Source: https://github.com/rubygems/rubygems/blob/master/lib/rubygems/version.rb#L360

  def <=>(other)
    return self <=> self.class.new(other) if (String === other) && self.class.correct?(other)

    return unless Gem::Version === other
    return 0 if @version == other.version || canonical_segments == other.canonical_segments

    lhsegments = canonical_segments
    rhsegments = other.canonical_segments

    lhsize = lhsegments.size
    rhsize = rhsegments.size
    limit  = (lhsize > rhsize ? lhsize : rhsize) - 1

    i = 0

    while i <= limit
      lhs = lhsegments[i] || 0
      rhs = rhsegments[i] || 0
      i += 1

      next      if lhs == rhs
      return -1 if String  === lhs && Numeric === rhs
      return  1 if Numeric === lhs && String  === rhs

      return lhs <=> rhs
    end

    0
  end
Enter fullscreen mode Exit fullscreen mode

As you can notice it knows also how to compare with a String so you could probably write this code like this:

if Rails.env.local? || Rails.env.ci?
  if Gem::Version.new("1.0.1") <= MyGem::VERSION
    raise "\n\nReview this monkey patch after upgrading \n" \
          "Path: #{__FILE__}"
  end
end
Enter fullscreen mode Exit fullscreen mode

But I prefer to make sure that both of them are Gem::Version objects.


If you like this article:

👐 Interested in learning how to improve your developer testing skills? Join my live online workshop about goodenoughtesting.com - to learn test design techniques for writing effective tests

👉 Join my Short Ruby Newsletter for weekly Ruby updates from the community

🤝 Let's connect on Bluesky, Ruby.social, Linkedin, Twitter where I post mostly about Ruby and Ruby on Rails.

🎥 Follow me on my YouTube channel for short videos about Ruby/Rails

Top comments (0)