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
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
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
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
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
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)