Down & Dirty with MongoDB Part 2: Ruby ToDo

Earlier this summer, we kicked off a series on MongoDB where the goal was to write a simple todo application using native MongoDB drivers and three of our favorite scripting languages.

This article is part 2 in that series, in which we write said application in Ruby.

The Interface

You can refer back to the introductory article for all the details, but I’ll reiterate the command-line interface here for easy reference:

  • todo – list all incomplete tasks sorted by priority then chronologically
  • todo next – list all incomplete tasks that are high priority
  • todo done – list all complete tasks chronologically
  • todo high “pay bills” – add high priority task called “pay bills”
  • todo low “get milk” – add low priority task called “get milk”
  • todo finish “pay bills” – complete task called “pay bills”
  • todo dont “get milk” – delete task called “get milk”
  • todo help – list all commands and their usage

Okay, let’s write this thing!

The Driver

10gen distributes the Ruby driver using Ruby’s most popular package distribution tool, RubyGems. To install the driver on your machine simply execute:

gem install mongo

Our program will need to load in the driver before anything else, and we’ll use RubyGems to do it, so create a new text file called mongo_todo.rb and begin it with the following:

require 'rubygems'
require 'mongo'

The ToDo Class

Ruby is a purely object-oriented language, so our program will employ a single object of a class we’ll write called ToDo. The class will take in the required command-line arguments, connect to the MongoDB collection that stores our todos, and execute the appropriate command on them.

To facilitate these needs, our object will need to take some arguments from the user and store a reference to the MongoDB collection when it is instantiated:

class ToDo
  def initialize(args)
    @args  = args
    @mongo = Mongo::Connection.new.db("todo").collection("todos")
  end
end

In Ruby, the initialize instance method is called when a new object of a class is instantiated, most often using the new class method, like this:

my_todo = ToDo.new

When a new ToDo object is instantiated, our class will store the arguments passed in to it in an instance variable called @args and store a connection to MongoDB in an instance variable called @mongo. Mongo::Connection is a class provided by the mongo gem that we’re using.

Since our class expects some arguments passed in to the initializer, we need to provide them to the new method. The arguments will be read in from the command-line when the program is executed, and Ruby exposes these to our program via the ARGV variable. So, we call new like this instead:

my_todo = ToDo.new(ARGV)

At this point, the entire program should look like this:

require 'rubygems'
require 'mongo'

class Todo
  def initialize(args)
    @args  = args
    @mongo = Mongo::Connection.new.db("todo").collection("todos")
  end
end

ToDo.new(ARGV)

You can execute this program from the command-line, but nothing will happen. Well, stuff will happen but you won’t see it displayed back to you. We still need to implement the bulk of the program, which parses the user’s command-line arguments and acts appropriately.

A Little Help

The first thing we’ll implement is a method that prints usage help for the program. It looks like this:

def help
  abort <<-HELP
  usage: #{__FILE__} <method>

  == Methods ==
  <none>            list all incomplete tasks sorted by priority
  help              show this help
  next              list all incomplete tasks that are high priority
  done              list all complete tasks chronologically
  high "argument"   add high priority task called argument
  low "argument"    add low priority task called argument
  finish "argument" complete task called argument
  dont "argument"   delete unfinished task called argument
  HELP
end

In Ruby, abort is a method you can call which will exit the application and display a string you pass to it. The string we’re passing in shows all the ways the program can be used, which will be displayed back to the user. This will be executed by default if the user doesn’t pass in an argument that we want to handle. More on that in the next section.

Run, Baby, Run

At some point, our application needs to run, so lets create an instance method called run which will handle the program’s flow. In this method we will determine what the user passed in from the command-line and call another method that handles the different use cases. The default action is to list all incomplete tasks, and the catch-all is to execute the help method we implemented above. Here is what run looks like:

def run
  if @args.empty?
    show :complete => false
  else
    case @args.first
    when "help"   then help
    when "high"   then add "high"
    when "low"    then add "low"
    when "next"   then show :complete => false, :level => "high"
    when "done"   then show :complete => true
    when "dont"   then complete false
    when "finish" then complete true
    else
      help
    end
  end
end

ARGV is an Array, so we can call methods like empty? and first on it. The meat of the run method is a Ruby case statement which checks if the first argument passed in by the user matches any of our predetermined keywords and calls other methods, add, show, and complete. These methods are the ones that actually interact with MongoDB and display information back to the user. We’ll implement them next.

Using Mongo

So far we’ve only dealt with setting up our class and parsing user arguments, we’ve barely even touched MongoDB! This is the meat of the application, so let’s get straight to it. First, the show method:

def show(params)
  sort = [["level", Mongo::ASCENDING], ["added", Mongo::ASCENDING]]
  @mongo.find(params, :sort => sort).each { |todo| puts "#{todo["task"]} (#{todo["level"]})" }
end

show takes a hash of params as an argument and passes that hash on directly to the mongo library’s find method, which will return an array of the found items in the collection.

We print each of the todo items returned from find along with the level of the todo. How do those items get in there in the first, place? That is up to the add method, which we will implement next.

def add(level)
  help unless @args.length == 2
  @mongo.insert "task" => @args.last, "level" => level, "complete" => false, "added" => Time.now
  puts "added #{level} level task: #{@args.last}"
end

add takes a single argument, the level of the todo to create (e.g.- “high” or “low”). It then makes sure that the user had passed in two arguments at the command line because it needs the second one for the name of the task. If not, it calls help which aborts the application.

If so, It calls Mongo’s insert method and passes it all the information to be stored in the datastore.

Finally, we need to implement the complete method, which is the most complex of the lot. Like add, it makes sure there were two command-line arguments given. Then it tries to find the todo by the “task” name. If found, it will either “finish” the task or “remove” the task depending on a boolean argument passed in. The method looks like this:

def complete(finish)
  help unless @args.length == 2
  if (document = @mongo.find_one("task" => @args.last, "complete" => false))
    if finish
      document["complete"] = Time.now
      @mongo.save(document)
      puts "finished: #{@args.last}"
    else
      @mongo.remove(document)
      puts "removed: #{@args.last}"
    end
  else
    puts "No ToDo matching #{@args.length} found"
  end
end

The mongo-related calls in the complete method are find_one, save, and remove.

Finishing Up

So, we’ve written our run method which calls the other methods using the user’s passed in arguments. Now all we have to do to get the class going is finish the program by creating a ToDo class object and calling run on it, like this:

ToDo.new(ARGV).run

You can view/download the program in its entirety here. One thing to note is how little code is needed to add CRUD to an application when using MongoDB as a datastore.

Hopefully this article has helped you with Ruby or MongoDB or both. Stay tuned for our next article in the series when we write the exact same application in PHP!

Jerod Santo is an Editor at Fuel Your Coding and a contract software developer at RSDi where he works daily with Ruby, JavaScript, and related technologies. He loves shiny toys, powerful tools, and open-source software. Learn more about Jerod by visiting his homepage or following him on Twitter.

 

If you liked this article, please help spread the news on the following sites:

  • Bump It
  • Blend It
  • Bookmark on Delicious
  • Stumble It
  • Float This
  • Reddit This
  • Share on FriendFeed
  • Clip to Evernote