In Progress
Unit 1, Lesson 1
In Progress

Gem Love Part 1

Video transcript & code

A few years ago I got an idea for a gem, called gem-love. The idea was to give Ruby programmers an easy way to show their appreciation for particularly useful libraries. It was going to have two parts: a command-line program, and a publicly accessible website.

Over the course of a weekend code camp in Rhode Island I hacked out about three quarters of it. Then I went home and never found the time to finish it.

Today I'm starting a new ongoing, occasional series wherein I revisit the gem-love project. I'm going to re-start the project from scratch, and document the process of development from start to finish. Along the way, it should give me an opportunity to explore topics like how I approach test-driven development, and how to design a client-server system.

I start the project with a question. I know that I want to be able to trigger my program by typing "gem", "love", and then the name of a gem that I love.

gem love fattr

I also know that this is possible through the use of the RubyGems plugin API. But I've never written a RubyGems plugin before. And one thing I'm concerned about is how I will test the integration. I don't want to have to reinstall my gem every time I want to test a change to the code.

So I start out with a code spike to explore this question. I begin by creating the directory and file that RubyGems expects when searching for plugins. I create a lib directory, and inside it I create a rubygems_plugin.rb file.

Inside the file, I require rubygems/command_manager. Then I register my plugin by invoking register_command on the Gem::CommandManager.

require 'rubygems/command_manager'

Gem::CommandManager.instance.register_command :love

Telling RubyGems about this new command sets up some very specific expectations about where the command implementation can be found. To satisfy those expectations, I create a rubygems directory inside the lib directory. Inside that I create a commands directory. And in that directory I create a new file called love_command.rb.

class Gem::Commands::LoveCommand < Gem::Command
  def initialize
    super 'love', "Tell the world of your love for a gem"
  end

  def arguments
    "GEM_NAME           the name of the gem you wish to endorse"
  end

  def usage
    "#{program_name} GEM_NAME"
  end

  def description
    <<END
Records your appreciation for a gem on gemlove.org.
END
  end

  def execute
    puts "Under construction..."
  end
end

This file defines a class named Gem::Commands::LoveCommand, which inherits from Gem::Command. The class' initializer registers the command's name and description by invoking the superclass initializer. The #arguments method returns documentation for the command's arguments. The usage method returns documentation of how the command should be invoked. The #description method is intended to return a longer, more detailed explanation of the command. Finally, the actual implementation of the command goes in the #execute method. For now, I just put a placeholder there.

Now that I have a skeleton RubyGems plugin, I want to see if I can temporarily register the working code with RubyGems. So I go to the command line, and try an experiment. I prefix my gem command with an environment variable assignment. The variable I'm assigning is the RUBYLIB variable, which contains a list of directories that Ruby should search when trying to load a library.

I include the current value of RUBYLIB in the new value, but I prepend my project's lib directory. Then I type out the rest of my command: gem help commands, which should give me a list of all registered commands.

Sure enough, my "love" command shows up in the list, along with its description! When I ask RubyGems for more information about the "love" commands, it outputs the info I put into the command class. And when I try to execute the "gem love" command, I get my placeholder text.

petronius% RUBYLIB=./lib:$RUBYLIB gem help commands
GEM commands are:

    build             Build a gem from a gemspec
    cert              Manage RubyGems certificates and signing settings
    check             Check installed gems
    cleanup           Clean up old versions of installed gems in the local
                      repository
    contents          Display the contents of the installed gems
    dependency        Show the dependencies of an installed gem
    environment       Display information about the RubyGems environment
    fetch             Download a gem and place it in the current directory
    generate_index    Generates the index files for a gem server directory
    help              Provide help on the 'gem' command
    install           Install a gem into the local repository
    list              Display gems whose name starts with STRING
    lock              Generate a lockdown list of gems
    love              Tell the world of your love for a gem
    mirror            Mirror a gem repository
    outdated          Display all gems that need updates
    owner             Manage gem owners on RubyGems.org.
    pristine          Restores installed gems to pristine condition from files
                      located in the gem cache
    push              Push a gem up to RubyGems.org
    query             Query gem information in local or remote repositories
    rdoc              Generates RDoc for pre-installed gems
    search            Display all gems whose name contains STRING
    server            Documentation and gem repository HTTP server
    sources           Manage the sources and cache file RubyGems uses to search
                      for gems
    specification     Display gem specification (in yaml)
    stale             List gems along with access times
    uninstall         Uninstall gems from the local repository
    unpack            Unpack an installed gem to the current directory
    update            Update the named gems (or all installed gems) in the local
                      repository
    which             Find the location of a library file you can require

For help on a particular command, use 'gem help COMMAND'.

Commands may be abbreviated, so long as they are unambiguous.
e.g. 'gem i rake' is short for 'gem install rake'.
petronius% RUBYLIB=./lib:$RUBYLIB gem love --help  
Usage: gem love GEM_NAME [options]


  Common Options:
    -h, --help                       Get help on this command
    -V, --[no-]verbose               Set the verbose level of output
    -q, --quiet                      Silence commands
        --config-file FILE           Use this config file instead of default
        --backtrace                  Show stack backtrace on errors
        --debug                      Turn on Ruby debugging


  Arguments:
    GEM_NAME           the name of the gem you wish to endorse

  Summary:
    Tell the world of your love for a gem

  Description:
    Records your appreciation for a gem on gemlove.org.
petronius% RUBYLIB=./lib:$RUBYLIB gem love fattr 
Under construction...
petronius% 

This is a good start! Now I know how to write a RubyGems plugin. And I know that in order to test my code end-to-end, I can just make sure that the project lib directory is included in RUBYLIB and the gem command will pick it up.

This exercise has been a code spike, where I write some quick, untested code for the purpose of research. Normally I would throw away a code spike after learning what I need to know. But in this case, the code is pretty innocuous—there's nothing in here that's interesting enough to be worth testing anyway. So I'll keep it around and build on it this time.

In writing this code I was greatly assisted by a blog post from Gabriel Horner, as well as by the RubyGems API documentation. ... ...

OK, that's enough for today. Happy hacking!

Responses