Gem::Version != SemVer

Using dependencies with semantic versioning (also known as SemVer) is a key piece of modern software development, and if we’re writing Ruby code that checks the version of a dependency, we often use Gem::Version for our operations.

The initializer for Gem::Version checks if we’ve passed in a valid version number (such as 1.9.10), and raises an ArgumentError if it’s invalid:

$ irb
3.0.0 :001 > Gem::Version.new('1.9.10')
 => #<Gem::Version "1.9.10"> 
3.0.0 :002 > Gem::Version.new('foo bar car')
ArgumentError (Malformed version number string foo bar car)

If we wanted to test this behaviour in RSpec, it would look something like so:

Most dependencies use release numbers with the MAJOR.MINOR.PATCH pattern (eg. 1.9.10), but the SemVer allows more complex identifiers than that. For a list of interesting edge cases that illustrate what is and isn’t allowed, the SemVer specification links to a Regex101 page containing a list of test strings.

Let’s pass those into Gem::Version via RSpec and see what happens! In the code below, we pass in each valid test string into the initializer for a new Gem::Version, and check that it successfully creates a Gem::Version (ie. doesn’t raise an error).

We actually receive an ArgumentError if we pass in a version number with metadata (ie. everything after the + sign), so we strip that out before passing that in. It would be ideal if Gem::Version did that metadata parsing for us, but ignoring that edge case, all of our test cases pass:

What happens when we look at the invalid test cases? Let’s look at the RSpec below:

We expect to receive an ArgumentError if we’re passed in an invalid version number, which we check for in our expect statements. We also skip some of our test cases with this snippet:

.reject { |version_string| version_string.include?('+') }

We skip these cases because we already know that Gem::Version doesn’t handle metadata correctly. Let’s run the above RSpec tests:

We see that a lot of version identifiers that are invalid under the SemVer specification don’t actually raise errors in these tests, and can be used to instantiate a Gem::Version. For example, Gem::Version.new('1.2.3.DEV') creates a new object and doesn’t raise an error, even though that’s not a valid version number under SemVer.

As it turns out, Gem::Version (ie. the versioning used by RubyGems) is much more permissive than the SemVer specification. This means that if we’re trying to adhere to the strictest definition of a SemVer version number, we can’t actually use Gem::Version to determine if a version string is valid due to this permissiveness. We can only use Gem::Version to tell us if a version string is invalid (barring the aforementioned exception around metadata and the + sign).

My favourite behaviour of Gem::Version isn’t actually covered in the negative test cases of the SemVer spec. Gem::Version lets us be arbitrarily granular with the number of dots in our version number. This isn’t valid in the SemVer specification either, but Gem::Version will happily parse it and do version comparisons with it:

Start your journey towards writing better software, and watch this space for new content.