An Introduction to RubyGems and Bundler

One of the greatest pleasures of working with Ruby on Rails is the large volume of quality libraries that are available to work with. As a developer, you can stitch together a web app using mostly off-the-shelf components. The speed with which you can turn out a prototype — or even a completed product — is phenomenal.

These days, I enjoy working with NodeJS/Express and Elixir/Phoenix. However, these solutions do not yet have the high quality and mature ecosystem that the Ruby and Rails communities have fostered. (Though they are quickly catching up). It is for this reason that I keep coming back to Rails when I need to get things done.

Ruby on Rails is my workhorse.

RubyGems

There's a saying in the Drupal community. It goes like this: "There's a module for that". Well, there just so happens to be an equivalent saying in the Ruby community: "There's a gem for that".

Gems are Ruby libraries that are packaged in a standard format. Gems are installed by the RubyGems package manager. You can install a gem from the command line like this:

gem install sinatra

Where did that gem come from? When you install gems they come from RubyGems.org. RubyGems.org is a gem hosting service. For readers in the Southern Ontario and Western New York area, you may be interested to know that RubyGems.org was created by Nick Quaranto from Buffalo, NY back in 2009.

Before we proceed, just to be clear: gems are code libraries installed by the RubyGems package manager which basically just downloads gems from RubyGems.org and puts them in a folder on your computer. That folder is in Ruby's $LOADPATH. The require statements looks in the $LOADPATH to pick up new code. So when you require a library in your code i.e. require 'sinatra', everything just works.

Bundler

There's also this cool Ruby program called Bundler. You can use it with RubyGems. It provides a means for specifying all of your project's dependencies in one file and installing them with one command.

You can specify your project's dependencies in a Gemfile like this:

source 'https://rubygems.org'

gem 'sinatra', '~> 1.4.6'
gem 'sqlite3'

This code would placed in a file called Gemfile in the root of your project. You can install the dependencies by running:

bundle install

Installing Gems from Other Sources

I'd be remiss to mention that you can install gems from sources other than RubyGems using Bundler. This is particularly useful if you want to install gems that are in development and thus not yet ready to be published on RubyGems.org.

You can depend on a gem from a Git repository by specifying the Git remote.

gem 'sinatra', git: 'https://github.com/sinatra/sinatra.git'

When depending on a gem from a Git repository, you can also specify the :tag, :branch, or :ref.

gem 'sinatra', git: https://github.com/sinatra/sinatra.git, branch: '2.2.0-alpha'

Finally, you can even depend on a gem from your local file system.

gem 'some_library', path: './vendor/some_library'

Versioning

When specifying dependencies it is very important to specify the version you want to use. You likely built your code against a specific version of a library and there is no way you can guarantee that it will work with future versions of that library.

Good news. Bundler can help us do that. But we need to know a bit about semantic versioning to understand how it all works.

Most gems use semantic versioning. Semantic versioning is a versioning scheme whereby you create version numbers that adhere to the following standard: MAJOR.MINOR.PATCH.

  1. MAJOR Bumped when you make breaking changes. Not backwards-compatible.
  2. MINOR Bumped when you add new functionality but remain backwards-compatible.
  3. PATCH Bumped when you fix bugs. Backwards-compatible.

In our Gemfile example we used the ~> symbol. This symbol is called a pessimistic version constraint. There is also an optimistic version constraint represented by >=.

Pessimistic and optimistic version constraints are easiest to illustrate by way of example. Lets assume that Sinatra 2.01.51.4.8 and 1.4.6 are released.

If we specify gem 'sinatra', '>= 1.4.8' in our Gemfile, Bundler will install version 2.0. According to semantic versioning, this means that we will be installing a new version of Sinatra that is not backwards compatible. This will almost surely break our application. Best not to do that.

We can specify gem 'sinatra', '>= 1.4.8', '< 2.0' which will install the latest version up to — but not including — version 2. That would result in bundler installing version 1.5. We could achieve the same by using pessimistic versioning and putting gem 'sinatra', '~> 1.4' in our Gemfile. The problem here is that, according to our semantic versioning rules, we will be installing a version of the library that includes new features. When we wrote our application, we did not know about or use these new features. Thus, this seems like an unnecessary risk to take.

Pessimistic versioning will install the latest version up to the point release specified. So if we add an additional point to the version number, we can be more conservative in our upgrade path. By specifying gem 'sinatra', '~> 1.4.6' in our Gemfile, Bundler will install version 1.4.8. We can be pretty confident that our app will work with version 1.4.8 which should only include bug fixes; no new features or breaking changes. This level of specificity strikes an excellent balance between getting bug fixes and having a stable code base.

The following version constraints are available for use in your Gemfile.

  • ~> Pessimistic versioning. The latest MINOR or PATCH release depending on granularity.
  • >= Optimistic versioning. Greater than or equal to the version.
  • > Greater than the version.
  • < Less than the version.
  • <= Less than or equal to the version.
  • != Not equal to the version.
  • = Equal to the version.

Now that you have seen those, forget about all of them except ~>. Most of those constraints are just plain strange to use when declaring dependencies. You're best bet is to use pessimistic versioning with the full MAJOR.MINOR.PATCHpattern.

Grouping

I feel like we can't discuss RubyGems and Bundler without discussing grouping too. One useful application of this is to depend on gems in different environments. Many web applications, including those built on top of Ruby on Rails, share a convention of having development, test, and productionenvironments. By default, all gems included in the Gemfile are added to the default group. However, we can also specify a group for any gem.

gem 'rspec', group: [:development, :test]

group :production do
  gem unicorn
end

Gems can be added to one or more groups via the group option or by including the gems in a group block. Both methods are included in the above example.

Doing this has two main benefits:

  1. We can install gems only from the groups we plan on working with on a specific machine. For example, on a development machine, we can run bundle install --without production to skip installing gems from the production group. On a server, we could run bundle install --without development test to only install gems from the default and production groups.

  2. We can use Bundler.require to automatically require all the gems in our Gemfile scoped to a specific group. Thus we could include Bundler.require(:default, ENV['RACK_ENV'] %>) in our code to dynamically require all the gems in the default group and whatever group that matches the name of the environment under which our application is current running. For example, if the value of RACK_ENV was development than this statement would require all gems in the default and development groups, avoiding loading libraries for testing and production, thereby reducing bloat.

Requiring Gems Installed Via Bundler

Any gem installed through Bundler is installed into your $LOADPATH and can thus be required in any source file using the require statement. After which you can proceed to use the library. For example, take this famous Sinatra example.

require 'sinatra'

get '/hi' do
  "Hello World!"
end

get is a method from the Sinatra library which accepts a string and block as parameters.

Another useful approach is to automatically load all of the gems specified in the Gemfile. Lets say your Sinatra app had a Gemfile that looked like this:

source 'https://rubygems.org'

gem 'sinatra', '~> 1.4.6'
gem 'sinatra-views', '~> 0.4.1'

We could then use bundler.require to automatically require the Sinatra and Sinatra Views gems.

Bundler.require

get '/hi' do
  view :hi
end

view :hi do
  "Hello World!"
end

Remember when we talked about groups? You can use Bundler.require to require gems from only specific groups. Here's how we would require all gems in the default group (no group specified) as well as all gems in the group that has the same name as the environment our application is running under.

Bundler.require(:default, ENV['RACK_ENV'] %>)

It is also useful to know, that you skip gems being automatically required by Bundler.require by setting the :require option to false for a gem in your Gemfile. This way, you will manually have to require yourself.

source 'https://rubygems.org'

gem 'sinatra', '~> 1.4.6'
gem 'sinatra-views', '~> 0.4.1', require: false
Bundler.require
require 'sinatra-views'

get '/hi' do
  view :hi
end

view :hi do
  "Hello World!"
end

One final note here. If a gem's main file name is different than the gem name, you need to tell Bundler the name of the file to require via the :requireoption.

gem 'gem-name-is-deceptive', require: 'main-file-name-does-not-match'

Did I mention there's a gem for that?

Coming back full circle. Wow, I went off on a tangent there. What's really cool about RubyGems and Bundler are the gems themselves. There are gems for just about everything you can imagine. And there are a lot of high quality gems for features you will likely need to implement when building a web app, including:

  • Analytics
  • Authentication
  • Authorization
  • Background processors
  • Database clients
  • Email
  • File uploads
  • HTTP clients
  • JSON parsers
  • Markup processors
  • Natural language processors
  • Object relational mappers
  • PDF generators
  • Web services
  • And much more

I will be reviewing useful gems for building web apps in Part 2: Essential Gems for Web Applications.

The thing about all these gems, is it's not just that they exist. These gems are often best in class, exhibiting the following characteristics:

  • Excellent documentation. Often including detailed readme file, user guides, and API documentation.
  • Complete or near complete test coverage.
  • Stable releases following semantic versioning.
  • Excellent idiomatic implementations.
  • APIs for customization and configuration.

Learning More

The ideas behind Gems, RubyGems, and Bundler are pretty straightforward. But there is much to learn on your way to mastery. The following are some resources to get you started.

And if your curious, you can check out this list of the all-time most downloaded gems.