Advertisement

Gem Creation with Bundler

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

Building a gem used to be a complex task that would require either a precise knowledge of the gem format, itself, or some dedicated tools to generate a suitable boilerplate. These days, we can use the excellent Bundler to remove this complexity and keep the amount of generated code to a minimum.


What We're Building

The test gem we're going to build is a dummy content generator you might use during development. Instead of generating "lorem ipsum" sentences, it uses Bram Stoker's Dracula to generate an arbitrary amount of sentences taken from the book. Our workflow will start by generating the gem, testing and implementing the minimum amount of code necessary to get our gem ready, and then publishing it on RubyGems.


Generating a Skeleton

I'm going to assume that you have a Ruby environment already setup. For this tutorial, we will use Ruby 1.9.3 as a baseline. If you plan, however, to develop a real gem, it might be good to also test it against Ruby 1.8 and other interpreters. For that purpose, a tool like Travis CI is a godsend; with a solid test suite in place, Travis will let you test your gem against a wide variety of platforms without any hassle. Let's start by generating the skeleton:

bundle gem bramipsum

I'm really sorry if you don't like the name I chose, as a matter of fact one of the hardest tasks in developing a gem is finding the right name. The command will create a directory, called bramipsum with a few files:

Gemfile

The Gemfile is very minimal:

source 'http://rubygems.org'

# Specify your gem's dependencies in bramipsum.gemspec
gemspec

Note that it clearly tells you to move your gem dependencies to bramipsum.gemspec, in order to have all the relevant data for your gem in the file that will be used to populate the metadata on Rubygems.

bramipsum.gemspec

The gemspec file contains a good deal of information about our gem; we can see that it relies heavily on Git to assign the right values to all the variables that involve file listing.

# -*- encoding: utf-8 -*-
require File.expand_path('../lib/bramipsum/version', __FILE__)

Gem::Specification.new do |gem|
  gem.authors       = ["Claudio Ortolina"]
  gem.email         = ["claudio@jumpzero.com"]
  gem.description   = %q{TODO: Write a gem description}
  gem.summary       = %q{TODO: Write a gem summary}
  gem.homepage      = ""

  gem.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  gem.files         = `git ls-files`.split("\n")
  gem.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  gem.name          = "bramipsum"
  gem.require_paths = ["lib"]
  gem.version       = Bramipsum::VERSION
  gem.add_development_dependency 'rake'
end

Next, we can run bundle to install Rake. As it was added as a development dependency, it won't be installed by Bundler when someone uses our gem.

A few interesting notes about the file:

  • It includes the Ruby 1.9 opening comment that specifies the file encoding. This is important, as some data in the file (like the email or the author name) can be a non-ascii character.
  • description and summary need to be changed to be correctly displayed on Rubygems.
  • The version is defined inside the lib/bramipsum/version file, required at the top. It defines the VERSION constant, called right before the end of the file.

The lib folder

The lib folder contains a generic bramipsum.rb file that requires the version module. Even if the comment in the file suggests that you add code directly to the file itself, we will use it just to require the separate classes that will form our small gem.


Updating the Base Data and Adding a Test Framework

Let's start by updating the data in bramipsum.gemspec:

...
gem.description   = %q{Random sentences from Bram Stoker's Dracula}
gem.summary       = %q{Generate one or more dummy sentences taken from Bram Stoker's Dracula}
...

Very simple stuff. Next, let's add support for proper testing. We will use Minitest, as it's included by default in Ruby 1.9. Let's add a test directory:

mkdir test

Next, we need a test_helper.rb file and a test for the presence of the Bramipsum::VERSION constant.

touch test/test_helper.rb
mkdir -p test/lib/bramipsum
touch test/lib/bramipsum/version_test.rb

Let's open the test_helper.rb file and add a few lines:

require 'minitest/autorun'
require 'minitest/pride'
require File.expand_path('../../lib/bramipsum.rb', __FILE__)

It requires both Minitest and Pride for colored output; then it requires the main bramipsum file.

The version_test.rb file needs to be updated with the following code:

require_relative '../../test_helper'

describe Bramipsum do

  it "must be defined" do
    Bramipsum::VERSION.wont_be_nil
  end

end

We use the expectation format for our tests. The test itself is fairly self-explanatory and can easily be run by typing:

ruby test/lib/bramipsum/version_test.rb

You should have a passing test!

Let's now update the Rakefile to have a more comfortable way to run our tests. Erase everything and paste the following code:

#!/usr/bin/env rake
require "bundler/gem_tasks"

require 'rake/testtask'

Rake::TestTask.new do |t|
  t.libs << 'lib/bramipsum'
  t.test_files = FileList['test/lib/bramipsum/*_test.rb']
  t.verbose = true
end

task :default => :test

This will let us run our tests by typing rake from the gem root folder.


Adding Functionality

As the focus of this tutorial is creating a gem, we will limit the amount of functionality we're going to add.


Base Class

Bramipsum is still an empty shell. As we want to use Dracula's book to generate sentences, it's time to add it to the repository. I have prepared a version of the book where I have removed any content, except the story itself: let's add it to the project.

mkdir -p book
curl https://raw.github.com/cloud8421/bundler-gem-tutorial/master/book/dracula.txt -o book/dracula.txt

Let's now create a Base class, where will add all the methods needed to extract data from the book.

touch lib/bramipsum/base.rb
touch test/lib/bramipsum/base_test.rb

The test file will have only a few expectations:

require_relative '../../test_helper'

describe Bramipsum::Base do

  subject { Bramipsum::Base }

  describe "reading from file" do

    it "must have a source" do
      subject.must_respond_to(:source)
    end

    it "must have the dracula file as a source" do
      subject.source.must_be_instance_of(String)
    end

  end

  describe "splitting into lines" do

    it "must correctly split the file into lines" do
      subject.processed_source.must_be_instance_of(Array)
    end

    it "must correctly remove empty lines" do
      subject.processed_source.wont_include(nil)
    end

  end

end

Running rake now will show an exception, as the base.rb file is still empty. Base will simply read the content of the file and return an array of lines (removing the empty ones).

The implementation is very straightforward:

module Bramipsum

  class Base

    def self.source
      @source ||= self.read
    end

    def self.processed_source
      @processed_source ||= self.source.split("\n").uniq
    end

    private

    def self.read
      File.read(File.expand_path('book/dracula.txt'))
    end

  end

end

We define a series of class methods that hold the original text and a processed version, caching the results after the first run.

We then need to open lib/bramipsum.rb and add the right require statement:

require_relative "./bramipsum/base"

If you save and run rake now, you should see all tests passing.


Sentence Class

Next, we need to add a new class to generate sentences. We will call it Sentence.

touch lib/bramipsum/sentence.rb
touch test/lib/bramipsum/sentence_test.rb

As before, we have to open lib/bramipsum.rb and require the newly created file:

require_relative "./bramipsum/base"
require_relative "./bramipsum/sentence"

This class will inherit from Base, so we can keep the implementation minimal. The test will need only three expectations:

require_relative '../../test_helper'

describe Bramipsum::Sentence do

  subject { Bramipsum::Sentence }

  it "must return a random sentence" do
    subject.sentence.must_be_instance_of(String)
  end

  it "must return 5 sentences by default" do
    subject.sentences.size.must_equal(5)
  end

  it "must return the specified amount of sentences" do
    subject.sentences(10).size.must_equal(10)
  end

end

The idea is that we can call Bramipsum::Sentence.sentence or Bramipsum::Sentence.sentences(10) to generate what we need.

The content for sentence.rb is also very concise:

module Bramipsum

  class Sentence < Base

    def self.sentence
      self.processed_source.sample
    end

    def self.sentences(n=5)
      self.processed_source.sample(n)
    end

  end

end

As we are on Ruby 1.9, we can use the sample method to return a random element from an array.

Once again, running rake should show all tests passing.


Building and Distributing the Gem

If you run gem build from the command line, a local copy of the gem will be built and packaged for you. If you don't need to distribute it, or if you need to keep it private, you can stop here. But if it's a project you can open-source, I encourage you to do that.

The obvious step is to add our brand new gem to RubyGems.org.

After creating an account on the site, visit your profile. You will find a command you need to run to authorize your computer. In my case it was:

curl -u cloud8421 https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials

You're now only one step away to publish the gem on Rubygems, however don't do it unless you really want to. The command you would run is:

gem push bramipsum-0.0.1.gem

Conclusion

Congratulations! You now know how to create a gem from scratch just using Bundler. There are however, other things to take into account:

  • Compatibility: you may want to support Ruby 1.8 as well. That will require refactoring all the require_relative calls; additionally, you will need to use the Minitest gem as it's not included by default in Ruby 1.8
  • Continuous Integration: you can add support to Travis CI and your gem will be tested in the cloud against all major Ruby releases. This will make it simple to be sure that there are no issues with platform-specific behavior changes.
  • Documentation: this is important, it's good to have RDoc comments that can help in generating automatic docs and a good README file with examples and guidelines.

Thanks for reading! Any questions?

Advertisement