135: Making a Gem
(view original Railscast)
Back in episode 33 [ watch, read ], we made a plugin for Rails. A more popular option for extending Rails’ functionality now is to make a gem, and that’s what we’re going to do in this episode.
Let’s say that we’d like to be able to generate a unique token for an ActiveRecord model. Given a model with a token
column, e.g. the Recipe
model below, we want to be able to call a class method called uniquify
that will set up a before_create
callback so that when we create a new Recipe it automatically generates a random token and ensures that it is unique in the database.
class Recipe < ActiveRecord::Base uniquify :token end
Creating a Gem
Ruby gems can be made from scratch, but this involves a lot of work. An easier way is to use another gem to help create ours. There are several of these including hoe, newgem, bones, gemhub and echoe. Most of them provide generators that will create the necessary files so that all we have to do is fill in the details. For the purposes of this episode we’re going to create the files manually and then use the echoe gem to package our gem.
Before we start writing our gem we’ll need to install echoe. This is installed in the same way as any other gem.
sudo gem install echoe
That’s all we need to enable us to begin writing our gem. We’ll start by creating a directory for it which will contain another directory called lib
.
mkdir -p uniquify/lib
Our gem will contain three files. We’ll use a README file (in rdoc format so that it renders correctly on GitHub) and we’ll also need a Rakefile and finally the Ruby file that will have the actual code in it. We can create these with touch
.
cd uniquify touch README.rdoc Rakefile lib/uniquify.rb
We’ll look at the Rakefile first. This is where we define our gem.
require 'rubygems' require 'rake' require 'echoe' Echoe.new('uniquify', '0.1.0') do |p| p.description = "Generate a unique token with ActiveRecord" p.url = p.author = "Eifion Bedford" p.email = "eifion@asciicasts.com" p.ignore_pattern = ["tmp/*", "script/*"] p.development_dependencies = [] end Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
In the Rakefile we’re using echoe to define the gem, passing in a name and a version number. In echoe’s block we define a number of parameters for the gem. The ignore_pattern
defines the patterns for the files we don’t want included in the gem. We don’t have tmp
and script
directories in our gem, but we’ve included them as an example. We’ve set the development_dependencies
to an empty array as by default echoe includes itself as a development dependency. Normally this isn’t a problem, but it can cause issues with older versions of RubyGems.
Finally we check in a tasks
directory for any .rake
files so that they can be loaded.
The Uniquify Code
We can now implement the gem’s functionality.
module Uniquify def self.included(base) base.extend ClassMethods end def ensure_unique(name) begin self[name] = yield end while self.class.exists?(name => self[name]) end module ClassMethods def uniquify(*args, &block) options = { :length => 8, :chars => ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a } options.merge!(args.pop) if args.last.kind_of? Hash args.each do |name| before_create do |record| if block record.ensure_unique(name, &block) else record.ensure_unique(name) do Array.new(options[:length]) { options[:chars].to_a[rand(options[:chars].to_a.size)] }.join end end end end end end end class ActiveRecord::Base include Uniquify end
It’s conventional to put a gem’s code into a module with the same name as the gem, so we’ve put ours into a module called Uniquify
. When our gem is included into ActiveRecord it will add the uniquify
method defined in the ClassMethods
module. This method has a before_create
callback which generates our unique token. Finally we include the Uniquify
module in ActiveRecord.
Now that the code’s written we’ll need some documentation, too. This belongs in the README.rdoc file we created earlier.
= Uniquify Rails gem for generating a unique token in an Active Record model. (rest of file omitted).
That’s all we need to create a Ruby gem. The only gem-specific part of the project is in the Rakefile, in the few lines of code related to echoe.
Testing
Now that we’ve written our gem how do we go about testing and publishing it? The answer is to use various rake tasks. As we’re using echoe the first task we want to run is rake manifest
.
$ rake manifest (in /Users/eifion/rails/uniquify) Cleaning Building Manifest + Manifest + README.rdoc + Rakefile + lib/uniquify.rb
This creates a Manifest file that stores a list of the files that will be in our gem. Next we can run rake install
to install the gem on our local machine. Once it’s installed it will appear in the list of gems on our machine like any other installed gem. We can then include it in a Rails application and test that it works as we expect.
$ gem list uniquify *** LOCAL GEMS *** uniquify (0.1.0)
Publishing
Once we know that our gem is working correctly we can publish it, either to RubyForge or GitHub. There are a number of rake tasks that will help us with this. If we have a RubyForge account set up we can run
rake release
to build the gem and upload it to RubyForge. We can then follow this with
rake publish_docs
to build and upload the documentation.
The steps are slightly different if we want to publish to GitHub. First we need to create a new Github repository.
Next we need to turn our gem’s directory into a git repository, which we can do by running
git init
There are some directories and files we don’t want to upload so we’ll create a .gitignore
file to define these. We want to ignore the pkg
and doc
directories and the Manifest file as these are generated by echoe, so our .gitignore
file will look like this:
pkg doc Manifest
We can now add our files to the repository and make our first commit.
$ git add . $ git commit -m "Initial import" [master (root-commit) cbfe307] Initial import 4 files changed, 57 insertions(+), 0 deletions(-) create mode 100644 .gitignore create mode 100644 README.rdoc create mode 100644 Rakefile create mode 100644 lib/uniquify.rb
That done we can add our remote repository and push the files to it.
$ git remote add origin git:eifion/uniquify.git $ git push origin master
Our project is now on Github but there are a couple of things we need to do to turn our project into a gem. The first is to edit the project settings and click the RubyGem checkbox.
We’ll also need to create a gemspec file. As we’re using echoe this is easy, we just need to run a rake task.
$ rake build_gemspec (in /Users/eifion/rails/uniquify) Gemspec generated
Note that the build_gemspec
task won’t appear in the list of available tasks when you run rake -T
, but it is a valid task. If we look in our directory now we’ll see the gemspec file.
$ ls Manifest Rakefile pkg README.rdoc lib uniquify.gemspec
To finish we’ll need to upload the gemspec file to Github.
$ git add . $ git commit -m "Adding gemspec file" [master edfcc2f] Adding gemspec file 1 files changed, 30 insertions(+), 0 deletions(-) create mode 100644 uniquify.gemspec $ git push Counting objects: 4, done. Delta compression using up to 2 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 818 bytes, done. Total 3 (delta 1), reused 0 (delta 0) To git:eifion/uniquify.git cbfe307..edfcc2f master -> master
Our project is now considered a gem on Github and we just have to wait a short while for it to build the gem.
That’s pretty much all we need to do to create a basic gem. As we develop the gem we’ll want to expand on this and add files as necessary. A good practice is to add a CHANGELOG file to keep track of the gem’s changes as we release new versions.
Releasing a new version of our gem is easy. All we need to do is change the version in our Rakefile then run
rake manifest
followed by
rake build_gemspec
We can then upload our new version to Github.
A Final Tip
We’ll wrap this episode up with a tip. If we want our gem to work as a plugin too we can do that by adding an init.rb
file. All we need to do in the file is require our gem, using the name of the file in the lib directory.
require 'uniquify'
This will load the file when it’s inserted into a Rails project as a plugin. Once the file has been pushed to your repository we can run
script/plugin install git://github.com/eifion/uniquify.git
to install Uniquify as a plugin instead of a gem.