Tools and Libraries - Effective Ruby: 48 Specific Ways to Write Better Ruby (Effective Software Development Series) (2015)

Effective Ruby: 48 Specific Ways to Write Better Ruby (Effective Software Development Series) (2015)

7. Tools and Libraries

An installation of Ruby gives you more than just an interpreter to execute your source files with. It also includes an interactive tool for experimenting with Ruby (IRB), a utility for reading documentation (RI), and one for generating documentation (RDoc). Mastering these tools is an important step for Ruby programmers. But there’s another, equally important tool that ships with Ruby, the gem utility.

There’s an amazingly large number of libraries and applications packaged as Ruby Gems just waiting to be installed with gem. In fact, it’s quite common for Ruby applications to depend on many third party gems. Managing all of those dependencies can be troublesome if you’re not using a tool to help you.

In this chapter I’ll show you how to get the most from Ruby’s standard tools such as IRB and RI, and also how to use the Bundler tool to manage your gem dependencies.

Item 40: Know How to Work with Ruby Documentation

Throughout our time together I’ve been telling you to consult the documentation for the so-and-so class or the whatsit module, but how do you do that? Where does this documentation come from? Can you document your own code in a similar way? Do I normally ask this many questions in a row?

The core and standard Ruby libraries come with excellent documentation and examples. Packaged along with Ruby are two tools for working with this documentation, let’s tackle them one at a time. The first tool, RI, is a terminal-based tool for viewing “Ruby Information”. With it you can view documentation for classes, modules, methods, and even the documentation for entire gems. Suppose you wanted to know more about the Array class. To view its documentation you just need to open a terminal window and ask RI:

ri Array

That command will bring up everything you ever wanted to know about the Array class, including any modules it includes and a list of its class and instance methods. You can even see if any installed gems have monkey patched Array to add documented methods. Here are some examples of the various types of names you can give to the ri utility:

• File::open – The open class method of the File class.

• Date::new – The new class method of the Date class. This is a special case since the documentation for the new class method actually comes from the initialize instance method. (I’ll talk more about this shortly.)

• Time#hour – The hour instance method of the Time class.

• clear – Any class or module which has a clear method will be listed with accompanying documentation.

• bundler: – List all files that are part of the bundler gem. (Don’t forget the trailing colon.)

• bundler:README.md – View the README.md file from the bundler gem.

If you don’t give a name as an argument to the ri utility it will enter interactive mode, prompting you for a name. This allows you to input partial names and have them automatically completed when you press the tab key, along with some other tricks. For more information about the RI tool you can read its documentation by typing the following into a terminal window:

ri --help

The final tool we’ll explore is RDoc, a utility for automatically generating the files which are used by RI. It does this by extracting documentation from Ruby source files written using special formatting rules within code comments. The formatting rules are quite easy to understand so let’s jump right in by looking at a class with a small bit of documentation:

# Represents a version number with three components:
#
# * Major number (1 in +1.2.3+)
# * Minor number: (2 in +1.2.3+)
# * Patch level: (3 in +1.2.3+)
#
# Example:
# v = Version.new("10.9.16")
# v.major # => 10
class Version
...
end

Documenting something in your Ruby code is as simple as preceding it with a comment. The formatting rules are so unobtrusive that it’s both easy to read the documentation directly in the source code and simple to understand how the formatting is converted for RI. You can see that “*” can be used for bullet points in lists but what other formatting rules can you spot? An interesting one is the use of “+” to surround words which should be displayed in a monospaced font. You might have also noticed that the code in the example section of the documentation is indented to the right. This tells RDoc that the entire indented block of text should be displayed as code in a monospaced font. Here’s another example, this time documenting a method:

# Parses the given version string and creates
# a new Version object.
def initialize (version)
...
end

You can use all the same formatting rules for methods that you can with class definitions. An interesting thing about the initialize method is that it’s a private instance method that isn’t invoked directly by users of the class. When RDoc parses the comments for initialize it will export them as the documentation for the new class method instead. In other words, initialize doesn’t show up in the documentation for the class, only new does.

The style of formatting that we’ve seen so far is known as—not surprisingly—the RDoc format. There are a handful of other RDoc special characters you can use to style your documentation, all of which are documented in the RDoc::Markup class (which you can read with RI). But the RDoc format isn’t the only one available. For example, RDoc can also parse documentation written in the Markdown format. While Markdown is becoming ubiquitous for all sorts of writing (including this book) most Ruby documentation is still written in RDoc format. I would venture to say that most Ruby programmers are still expecting to find RDoc formatted comments in Ruby source code. At least for the time being.

Once you’ve written documentation into your source code it’s time to use the rdoc utility to extract it. Since we’ve been focusing on reading documentation in RI let’s start by generating files which it can read. Open a terminal window and get yourself into the folder where your project files live. Then type the following command:

rdoc -f ri

This will recursively scan the current folder for any Ruby source files and build RI documentation for all of them. After it finishes, the files it produced will be in the “doc” folder. You can tell the ri utility to look in this folder for documentation files with the -d command line option (e.g. “ri -d doc”) or ask rdoc to automatically install the generated documentation into a central location using the -r command line option (e.g. “rdoc -rf ri”).

In addition to generating documentation for RI, RDoc can also generate HTML files. This is perfect for reading with a web browser and publishing the documentation somewhere on the web. The -f command line option to rdocspecifies which formatting engine to use. At the time of this writing RDoc ships with an HTML engine known as darkfish. In order to generate HTML files run this command instead of the previous:

rdoc -f darkfish

The generated HTML files will be placed in the “doc” folder. The entry point to the documentation is the “index.html” file. Like with the ri utility, a lot more information can be found by running rdoc with the --help command line option. Now go and make the world a better place by documenting your code!

Things to Remember

• The ri utility is used for reading documentation and the rdoc utility is used for generating it.

• Use the “-d doc” command line option with the ri utility to have it look in the “doc” folder for documentation.

• Generate documentation to be used with RI by running rdoc with the “-f ri” command line option. Alternatively, use “-f darkfish” to generate HTML.

• Complete documentation for the RDoc formatting rules can be found in the RDoc::Markup class (which you can read with RI).

Item 41: Be Aware of IRB’s Advanced Features

For many programmers, their very first exposure to Ruby was through the interactive Ruby shell known as IRB. I’m willing to bet yours was too. It’s not uncommon for programming languages to have a read-eval-print loop (REPL) utility. These incredibly useful tools turn the programming language into an interactive exploration environment, a place to experiment with language features and libraries. Ruby has an exceptionally wonderful REPL which provides a playground for novice and expert programmers alike. But many Ruby developers never go beyond that initial encounter with IRB to discover its many advanced features. That’s something I’m hoping to rectify here.

Over the next few pages I’ll introduce you to helpful IRB features and ways to customize it. The entry point for any customization to IRB is through the IRB configuration file. IRB expects this file to be in your home directory in a file named “.irbrc”. (Take note that the file name begins with a period.) Alternatively, you can put the file anywhere you want and then store its location in the IRBRC environment variable. Use which ever method works best on your operating system of choice.

The IRB configuration file is a plain old Ruby source file where you can do anything you’d normally do in Ruby. Changing various IRB settings is done through the IRB.conf method. It returns an internal hash which you can modify in order to change IRB’s behavior. For example, you can make IRB automatically indent code entered interactively by setting the :AUTO_INDENT option to true:

IRB.conf[:AUTO_INDENT] = true

A full list of IRB’s configuration options are included in its documentation. You can use them to customize the command prompt, control how many items are kept in the command history, and even enable tab key completion. Since this item is concerned with the more advanced features of IRB I won’t mention these simple configuration options further.

When working inside an IRB session everything you enter will be passed to the Ruby interpreter and evaluated. Even IRB commands—which resemble commands that you enter into a terminal window—turn out to be nothing more than Ruby methods. That’s a good thing because it means we can define our own IRB commands by defining methods. For example, let’s say we want to write an IRB command called time which can execute a block and then report how many seconds it took to run. One way to accomplish this is by adding an instance method to the Object class. Since IRB evaluates input in an anonymous object by default, our time method would be appear to be a top-level command in IRB. But this command would also become a method on every object in the current IRB session. That’s a bit messy. A better way is to exploit a trick from IRB itself.

The anonymous object where all input is evaluated extends the IRB::ExtendCommandBundle module. If you define an instance method inside that module it will be available as an IRB command without having to infect the Objectclass. (You could also define instance methods in another module and then include that module into IRB::ExtendCommandBundle. This would be more practical if you were writing a plug-in for IRB.) With that technique in mind, here’s an example definition of the time command which you could put into your IRB configuration file:

module IRB::ExtendCommandBundle
def time (&block)
t1 = Time.now
result = block.call if block
diff = Time.now - t1
puts("Time: " + diff.to_f.to_s)
result
end
end

Besides defining your own commands there are some really nifty IRB tricks that you may have missed. The one everyone wishes they knew earlier is the underscore (“_”) variable feature. After evaluating input, IRB stores the result of the executed expression in a variable named “_”. This is great if you forget to assign the expression to a variable:

irb> [1, 2, 3].pop
---> 3

irb> last_elem = _
---> 3

Another useful feature in IRB is sessions. You can think of sessions as being a way to start a new copy of IRB from within IRB. Each new session in IRB has its own evaluation context, you can set local variables in one session and they won’t affect another. What really makes them useful though is that you can zoom into any object, making it the current evaluation context and effectively changing which object self references. When working inside IRB, the irb command can be used to create a new session and takes an optional argument which becomes the evaluation context:

irb> self.class
---> Object

irb> irb [1,2,3]

irb#1> self.class
-----> Array

irb#1> length
-----> 3

Each session in IRB has a unique ID which allows you to manage all of the running sessions from within IRB. You can use the jobs command to list all of the sessions along with their assigned IDs. When given an ID, the fgcommand will make the session with a matching ID the active session. When you’re finished with a session you can use the exit command to terminate the active session or the kill command to terminate a session with a specific ID. There’s more to IRB sessions than what I have space for here, including another layer called workspaces. The documentation for IRB can be found in the IRB module which is part of the Ruby standard library.

One of IRB’s greatest strengths is that it’s part of Ruby. It’s also fairly easy to leverage the IRB library to build your own custom interactive developer tools, the Ruby on Rails console application being a perfect example. And as we’ve seen, it’s possible to extend IRB with new features. Over the years many plug-ins for IRB have been written to add features such as syntax highlighting and improved pretty printers. Unfortunately, most of them have suffered from bit rot and haven’t been updated to work with modern versions of Ruby.

If you’re looking for something more fancy than IRB you may want to consider another popular REPL for Ruby: Pry. This Ruby Gem has been exploding in popularity over the last couple of years due to its impressive feature list. Out of the box it matches and exceeds IRB in features. It also has a growing repository of plug-ins that add to Pry’s already long list of capabilities. When you’re ready to try an alternative to IRB, Pry is just a gem install away.

Things to Remember

• Define custom IRB commands in the IRB::ExtendCommandBundle module or a module which is then included into IRB::ExtendCommandBundle.

• Use the underscore (“_”) variable to access the result of the last expression.

• The irb command can be used to start a new session and change the current evaluation context to an arbitrary object.

• Consider the popular Pry gem as an alternative to IRB.

Item 42: Manage Gem Dependencies with Bundler

Newcomers to Ruby often marvel at the incredible number of libraries which are available as Ruby Gems. If you’re looking to add a new feature to one of your applications, you should start by searching the collection of available gems. It’s highly likely that someone has already done most of the legwork for you and released their code as a Ruby Gem. As the saying goes: “There’s a gem for that.”

As you know, introducing a gem to a new or existing Ruby project is very easy. It usually involves executing the gem utility to install the desired library on your development machine followed by an appropriate require line somewhere in your project. There’s even a built in way to ask Ruby to only load a specific version of a gem, in case you happen to have multiple versions installed. The Kernel#gem method takes the name of a gem and a version specification. It then ensures that a successive call to require will only load the correct version of the requested gem. This is a fairly light weight way to manage project dependencies. If it worked for anything other than the most trivial of Ruby projects there wouldn’t be anything else to read in this item. But of course, you know better than that.

Managing your gems manually is troublesome because it’s not very easy to keep track of all your dependencies in a reproducible manner. Your project might rely on a handful of gems, and in turn those gems might have dependencies on other gems, and so forth and so on. Trying to manage the dependency graph manually with the Kernel#gem method quickly gets out of hand, especially when you need to install specific versions of these gems on more than one machine. Thankfully, we have the Bundler gem to help us.

Bundler automatically manages the gem dependency graph and ensures that the exact set of gems you use during development is also used by all other developers and on the production servers as well. You specify the gems you need in a file named Gemfile. When instructed, Bundler will install all of the necessary gems and their dependencies. It also creates or updates a file named Gemfile.lock which contains the entire dependency graph. It’s this file—in conjunction with a source code version control system—that allows you to reproduce any version of your application along with its exact dependencies.

If you’ve used a large application framework such as Ruby on Rails you’re already familiar with how to use Bundler, at least partially. Rails handles a lot of the Bundler interaction behind the scenes for you. If you haven’t used Bundler outside of something like Rails then you might not be aware of how to integrate it into a non-Rails application. That’s where this item comes in.

In order to demonstrate using Bundler in a regular, everyday Ruby project, let’s peek at the important parts of a small program which uses a couple of gems. Suppose you’d like to extract the metadata from an MP3 audio file and print it out in the JSON format. The metadata in an MP3 file is stored in the ID3 tag format, so the first thing we’re going to need is a Ruby Gem which can work with MP3 files and ID3 data. Ruby already comes with a JSON library, but to ensure we’re working with the latest version we’ll also add it as a gem dependency. The very first step when starting out with Bundler is to install the bundler Ruby Gem. Just as with any other gem, Bundler is installed by opening a terminal window and running:

gem install bundler

Bundler comes with a library named bundler and a command line utility named bundle. The utility can be used to create a default Gemfile, which is what we’ll do now. From a terminal window run the following command:

bundle init

If you open the newly generated Gemfile you’ll quickly notice that it’s nothing more than a regular Ruby file and is mostly made up of comments. The only actual working line in the entire file looks something like this:

source("https://rubygems.org")

The source method in a Gemfile tells Bundler which repository of Ruby Gems you want to use. The URL in the default Gemfile is the official repository and the one you’ll most likely want to use. You can list as many gem repositories as you want by including additional source lines. This can be helpful if you want to run your own repository for private gems or as a mirror of the official repository. After telling Bundler which repositories you want to use you’ll of course want to list the gems you’re interested in. Suppose that for this simple ID3 to JSON converter you’ve decided to use the id3tag and json gems.

When specifying gems in a Gemfile you have a lot of options for indicating which versions of those gems you’re willing to accept. The gem method is used to add a dependency on an external library and has only one mandatory argument: the name of the gem. If the gem method is used without any additional details besides the name of the requested gem then Bundler will select the newest version of that gem. This is generally a bad idea because the next time you ask Bundler to update your gems you could potentially end up with newer versions of everything in your Gemfile. This may inadvertently introduce breaking changes to your application. You’re better off specifying an explicit version (or a range of versions). For now we’ll stick with specific versions of our gems but in Item 43 we’ll focus on safe ways to loosen the version requirements for dependencies. Adding the two gems we’ve selected for this project to the Gemfile is as simple as adding these two lines:

gem('id3tag', '0.7.0')
gem('json', '1.8.1')

After adding gems to the Gemfile you need to tell Bundler to install those gems along with all of their dependencies. This is where we turn back to the bundle command line utility. To install any missing gems open a terminal window and run the following command:

bundle install

In addition to installing any necessary gems, the bundle install command will generate a new Gemfile.lock file which includes every gem in the Gemfile as well as all their dependencies. Each gem is listed along with its exact version number. If the Gemfile.lock file exists when you execute bundle install then it will consider the version numbers listed in that file and only install those gem versions. As mentioned earlier, this ensures that your teammates and production servers will be using the same gem versions that you are. (If you want to upgrade a gem to a new version you’ll need to update the version number in the Gemfile and then run bundle update. There’s more about this in Item 43.)

Now you’re ready to use the gems specified in the Gemfile from within the application. There are two ways to do this depending on how much control you want over the process. Let’s start with the option that gives us the most flexibility and is the most familiar. All you have to do is require the “bundler/setup” file and then require the other gems like normal:

require('bundler/setup')
require('id3tag')
require('json')

Loading the “bundler/setup” file alters the environment your program is running in so that Ruby can find the gems we need without accidentally finding gems that haven’t been listed in the Gemfile. This includes restricting the gems to those specified in the Gemfile.lock file. After loading Bundler you’re free to require the other gems as you need them, all at once or sprinkled throughout the source of the program. Of course, if your Gemfile has a lot of gems then requiring each gem in your program can be a bit painful and introduces some duplication. In that case you have another option, ask Bundler to load all of the gems for you:

require('bundler/setup')
Bundler.require

Placing these two lines in your program will cause it to load Bundler and then every gem defined in the Gemfile. More specifically, when the Bundler::require method is called without any arguments it will load all of the gems in the Gemfile. However, you can give it one or more names of a gem group and then Bundler will only load the gems which are part of those groups. You can define groups in the Gemfile by using the group method:

group(:production) do
gem('id3tag', '0.7.0')
gem('json', '1.8.1')
end

Groups can be useful when you want to create environments where some gems are used and others are not. For example, it’s fairly common to create a test group where you list all of your testing gems. You can then instruct Bundler to install those gems on your development machines but not on the production servers. (For more information about deploying specific groups to your production servers see the documentation for Bundler.) Changing our program to only load the gems in the production group is pretty straight forward:

require('bundler/setup')
Bundler.require(:production)

Up to this point Bundler has been used to manage the dependencies for a program that you’re not planning on distributing. Being the warm, caring person that you are, you’ll probably want to share some of your code with the Ruby community at some point. When you do, the most accepted way is by wrapping that code up into a gem. You’ll still be able to use Bundler to help you manage the dependencies of your gem but things are a little bit different. Let’s take a short look at how to use Bundler to develop a Ruby Gem.

Earlier, when we wanted to create a default Gemfile, we used the bundle init command. In order to package your code up as a Ruby Gem you’re going to need a few more files. Thankfully, the bundle utility can also generate a skeleton project which is ready for packaging and distribution. You need to give Bundler a name for your new project so for this example let’s say you decided to use “mp3json”. From a terminal window run the following command:

bundle gem mp3json

This will instruct Bundler to create a mp3json folder and fill it with all sorts of useful files, including some source files to help get you started. Since we’re focusing on Bundler, the two files we’re interested in are the mp3json.gemspec file and the Gemfile. Let’s start with the familiar Gemfile. This time Bundler generated one that looks slightly different than what we’re used to:

source('https://rubygems.org')
gemspec

The line containing an invocation of the source method should be familiar, but what’s that gemspec method do? In order to create a package for your code the Ruby Gems system needs information about your gem, including its dependencies. All the details about your gem are written into a specification file, in our case it’s that mp3json.gemspec file I mentioned earlier. To avoid duplicating information, Bundler allows you to keep your dependencies in the gem specification file where they belong and use the gemspec method in your Gemfile to pretend they’re in the Gemfile too. This means that during development you can continue to use the bundle install command to install dependencies. In the gem specification file you use the Gem::Specification::new method to specify all the details about your gem such as its name, version number, description, etc. How to specify those fields is obvious once you open the generated gem specification file, but what’s not so obvious is how to add dependencies on other gems. Here’s an example using the gems from the MP3/ID3 project:

Gem::Specification.new do |gem|
gem.add_dependency('id3tag', '0.7.0')
gem.add_dependency('json', '1.8.1')
...
end

The Gem::Specification#add_dependency method is a lot like the gem method available in a Gemfile. The only required argument is the name of the gem you want to add as a dependency. But like before, we’ll give an explicit version number to make installing our gem a bit more predictable. (In reality you’ll probably want to loosen this restriction a bit, see Item 43 for more details.)

A final word of caution. When writing an application, it’s important to place the Gemfile.lock file in your version control system. That file allows you to ensure that deployments to production include the exact same gems which were used in development. The same is not true, however, when writing a library to be distributed as a Ruby Gem. In that case the general recommendation is to not include the Gemfile.lock file in your version control system. Doing so complicates the development process for a gem and keeps you from noticing incompatibilities between your gem and its dependencies.

Things to Remember

• In exchange for a little bit of flexibility you can load all of the gems specified in your Gemfile by using Bundler.require after loading Bundler.

• When developing an application, list your gems in the Gemfile and add the Gemfile.lock file to your version control system.

• When developing a Ruby Gem, list your gem dependencies in the gem specification file and do not include the Gemfile.lock file in your version control system.

Item 43: Specify an Upper Bound for Gem Dependencies

There are a few different ways to introduce a gem as a dependency in your project. In Item 42 we looked at these three methods: using Ruby Gems directly in your source code with the gem method, using Bunlder to list dependencies in a Gemfile, and using the add_dependency method in a gem specification. All three of these methods allow you to place restrictions on gems based on something called a gem requirement. This basically boils down to specifying a range of version numbers that your project is known to work with. Here’s something you’re likely to see in a Bundler Gemfile out in the wild:

gem('money', '>= 1.0')

This line adds a dependency on the money gem with a requirement that Bunlder selects at least version 1.0. Specifying a lower bound on the gem requirement without an upper bound means that you’ll accept the version listed and all future versions. Even though this is really common, it’s a receipt for disaster. Can you really say that your application will work with every future version of a gem? Of course not. But the harm might not be immediately obvious, especially since we have the Gemfile.lock file to protect us, right? Not quite.

Suppose that you’re in charge of the maintenance for an application which is a few years old. All of the gems in the Gemfile have version requirements which only contain lower bounds, no upper bounds. Following the advice from Item 42, you’ve committed the Gemfile.lock file to your source code control system. Now, let’s say you’ve spent a long night debugging this application and have tracked a problem down to one of the gems. You do some poking around and discover that the author of the buggy gem has released a new version which fixes the problem you’re seeing. In your excitement (and lack of sleep) you ask Bundler to update your gems by running the following command:

bundle update

Boom! All of your tests start failing. Guess what, you just updated all of your gems to their latest versions and some of them contained changes that are not backwards compatible. If you didn’t have your Gemfile.lock under version control you’d be in a world of hurt because Bundler also just updated it with all the latest version numbers. There are two ways to prevent something like this from happening. One really simple way is to make sure you only update one gem at a time. Bundler can be smart about introducing just a single update to the dependency graph by telling the bundle utility which gem you want to upgrade:

bundle update money

This will only get you so far though. If you’re packaging a library as a Ruby Gem you need to list the version requirements for your gem’s dependencies in the gem specification file. In this situation you won’t have control over the Gemfile being used by other programmers. Without an upper bound on your gem’s dependencies it might break for others when one of these dependencies introduces a breaking change. You can’t predict the future and omitting an upper bound on version requirements, whether in your Gemfile or somewhere else, is akin to saying that you can. To fix this, let’s explore the process of managing version requirements using explicit version numbers and how to introduce flexibility with a range of version numbers.

When developing an application your best bet is to manage your dependencies with Bundler and a Gemfile. Personally, I like to specify my gems with an explicit version like this:

gem('money', '5.1.1')

This way you share in the responsibility for managing gems. You’re in charge of maintaining the explicit version requirements for your direct dependencies and Bundler uses the Gemfile.lock file to maintain the dependencies of those gems. No gem can get updated without someone updating a version requirement in the Gemfile. Some people consider this a bit strict. If you’d like more flexibility you can specify a version range by giving more than one requirement:

gem('money', '>= 5.1.0', '< 5.2.0')

Specifying a range like this has become so common that there’s even a custom operator which offers a shortcut: the pessimistic version operator. It might look a bit strange at first but the previous version range can be expressed with the pessimistic version operator like this:

gem('money', '~> 5.1.0')

The pessimistic version operator creates a range of version numbers by manipulating the version string to its right. In this example the lower bound of the range turns into “>= 5.1.0”. The upper bound is created in two steps. First the rightmost digit is removed from the version string, so 5.1.0 becomes 5.1. Next, the new rightmost digit is incremented, changing 5.1 into 5.2. The resulting number becomes the upper bound: “< 5.2.0”. This sort of “fuzzy” behavior can be a little confusing. Because of that I prefer explicit lower and upper bounds to using the pessimistic version operator. And since the pessimistic version operator can’t express more detailed ranges it’s not really appropriate for our next task, specifying dependencies when writing a library.

Recall that when developing a Ruby Gem you use the add_dependency method in a gem specification to add a dependency on another gem. When someone installs your gem they’ll automatically download and install all of its dependencies. I mentioned earlier that I prefer explicit gem versions, but that doesn’t work at all in this scenario. An example might be helpful.

Suppose that you’re writing a gem which can fetch stock quotes and calculate rates of return. Naturally, you’ll want to use the money gem because doing arithmetic with money can be a bit goofy and the money gem is well established and tested. Let’s say you put the following in your gem specification:

Gem::Specification.new do |gem|
gem.add_dependency('money', '4.2.0')
...
end

You’ve fully tested your gem against version 4.2.0 of the money gem so that’s the version requirement you put in the gem specification. Continuing along with this example, suppose that somebody is writing an application to produce pretty return on investment charts and they want to use your nifty gem. They’re already using the money gem, except they need features that were introduced in version 5.0. See the problem? The person writing this charting app can’t use your gem because then their application would require two different versions of the money gem, and that’s not allowed.

If you’re going to release a gem to the public and it has dependencies on other gems, you have a responsibility of sorts to provide a flexible range of versions for those dependencies. The lower bound is easy, pick the earliest version of the dependency that you know to work correctly with your package. The upper bound it a little more tricky because you have a few different choices. Going against the advice in this item, you can simply omit the upper bound. That’s not very nice to the community though because you have no way of knowing that your gem is going to continue to work for all future versions of its dependencies. The right thing to do is supply a reasonable upper bound. This can either be the last known working version of the dependency or even a bit more flexible, up to (but not including) the next major release.

Let’s say you know that your gem works with version 4.2.0 of the money gem, and you’ve also tested it with version 5.1.1. You have reason to expect that the author of the money gem won’t introduce any breaking changes without changing the version number to 5.2.0 and therefore conclude that all of the 5.1.x releases will be safe. This allows you to write a much more flexible gem specification:

Gem::Specification.new do |gem|
gem.add_dependency('money', '>= 4.2.0', '< 5.2.0')
...
end

This wider range of version requirements makes your gem flexible enough to work with past and future versions of the money gem without taking on too much risk of breakage. It also means that you don’t have to release a new version of your gem each time the developer of the money gem releases a small patch version. I’d say that’s a good compromise.

Things to Remember

• Omitting an upper bound on a version requirement is akin to saying that your application or library supports all future versions of a dependency.

• Prefer an explicit range of version numbers over the pessimistic version operator.

• When releasing a gem to the public, specify dependency version requirement as wide as you safely can with an upper bound that extends until the next potentially breaking release.