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 = ["[email protected]"] 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
andsummary
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 theVERSION
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 theMinitest
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?
Comments