Saturday, August 8, 2009

CouchDocs: From the Command Line

‹prev | My Chain | next›

For a while now, couch_design_docs (extracted from my CouchDB / Sinatra application) has done more than play with CouchDB design documents. So the first thing I do today is to finally create a new couch_docs gem. It is copied directly from couch_design_docs. Now that I think about it, I probably should have forked it, but mostly the same result.

I reset the version number to 0.9. I will consider it 1.0 when I have a script to dump and load (backup and restore) a CouchDB database. So let's get started...

My couch-docs script (mostly generated by Bones):
#!/usr/bin/env ruby

require File.expand_path(
File.join(File.dirname(__FILE__), %w[.. lib couch_docs]))

# Put your code here

CouchDocs::CommandLine.run ARGV

# EOF
I do not expect to do much in the CommandLine, but, all the same, I create an instance—encapsulation helps once the command line gains any complexity. The run class method should instantiate and run a command line, or, in RSpec format:
describe CommandLine do
it "should be able to run a single instance of a command line" do
CommandLine.
should_receive(:new).
with('foo', 'bar').
and_return(mock("Command Line").as_null_object)

CommandLine.run('foo', 'bar')
end

it "should run the command line instance" do
command_line = mock("Command Line").as_null_object
command_line.
should_receive(:run)

CommandLine.stub!(:new).and_return(command_line)

CommandLine.run('foo', 'bar')
end
end
Those examples drive this implementation:
module CouchDocs
class CommandLine
def self.run(*args)
CommandLine.new(*args).run
end

def initialize(args)
end
end
end
With the preliminaries out of the way, it is time to drive the command line to actually do something. I want two different command line options:
# For dumping the contents of a CouchDB database to the filesystem
couch-docs dump "http://localhost:5984/db" path/to/dump_dir/

# For loading documents from the filesystem into CouchDB
couch-docs load path/to/dump_dir/ "http://localhost:5984/db"
To run the dump version:
  context "an instance that dumps a CouchDB database" do
before(:each) do
@it = CommandLine.new('dump', 'uri', 'dir')
end

it "should dump CouchDB documents from uri to dir when run" do
CouchDocs.
should_receive(:dump).
with("uri", "dir")

@it.run
end
end
When I execute that example, I get:
1)
NoMethodError in 'CouchDocs::CommandLine a "dump" instance should dump CouchDB documents from uri to dir when run'
undefined method `run' for #
./spec/couch_docs_spec.rb:300:

Finished in 0.019961 seconds
I change the message by defining an empty run method, to find:
1)
Spec::Mocks::MockExpectationError in 'CouchDocs::CommandLine an instance that dumps a CouchDB database should dump CouchDB documents from uri to dir when run'
CouchDocs expected :dump with ("uri", "dir") once, but received it 0 times
./spec/couch_docs_spec.rb:296:
To make that pass, I squirrel away the command and options in instance variables and use them in the run method to call the dump class method on CouchDocs:
    attr_accessor :command, :options

def initialize(*args)
@command = args.shift
@options = args
end

def run
case command
when "dump"
CouchDocs.dump(*options)
else
raise ArgumentError.new("Unknown command #{command}")
end
end
After following a similar path with the load option, I am done with the couch-docs script!

After installing the gem locally, the script is in my $PATH, I give it a try with the seed data that I dumped yesterday. Unfortunately:
cstrom@jaynestown:~/repos/eee-code$ couch-docs load couch/seed http://localhost:5984/seed
/usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:193:in `process_result': Resource not found (RestClient::ResourceNotFound)
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:125:in `transmit'
from /usr/lib/ruby/1.8/net/http.rb:543:in `start'
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:123:in `transmit'
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:49:in `execute_inner'
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:39:in `execute'
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient/request.rb:17:in `execute'
from /usr/lib/ruby/gems/1.8/gems/rest-client-1.0.3/lib/restclient.rb:65:in `get'
from /home/cstrom/.gem/ruby/1.8/gems/couch_docs-0.9.0/lib/couch_docs/store.rb:55:in `get'
from /home/cstrom/.gem/ruby/1.8/gems/couch_docs-0.9.0/lib/couch_docs/store.rb:50:in `delete'
from /home/cstrom/.gem/ruby/1.8/gems/couch_docs-0.9.0/lib/couch_docs/store.rb:38:in `delete_and_put'
from /home/cstrom/.gem/ruby/1.8/gems/couch_docs-0.9.0/lib/couch_docs/store.rb:34:in `put!'
from /home/cstrom/.gem/ruby/1.8/gems/couch_docs-0.9.0/lib/couch_docs.rb:45:in `put_document_dir'
...
To track this down, I check the CouchDB logs to find:
[info] [<0.14094.13>] 127.0.0.1 - - 'PUT' /seed/2002-01-12-squash_ravioli 409
Ah, nuts! The dump from yesterday includes the revision number of the document. That revision number is causing a conflict in the DB. I will need to revisit the dump to strip out that revision number.

I will call it a day at that point. In addition to stripping the revision from the document when it gets dumped to the file system, I also need to include the image attachments.

2 comments:

  1. HI,

    you have seen couchapp*, have you? :)

    * http://github.com/couchapp/couchapp/tree/master


    Cheers
    --

    ReplyDelete
  2. For sure.

    I mostly wanted something that can work with Ruby, since my app is written in Sinatra / Ruby. Why add another dependency in my app if I can avoid it?

    Although there is some overlap in functionality, I am mostly looking to manage documents, not manage an application.

    ReplyDelete