Sunday, July 12, 2009

When I Say put, I Mean put!

‹prev | My Chain | next›

As of last night, the CouchDB Store class in the couch_design_docs gem is capable of putting new documents in a CouchDB database. That functionality is sufficient for working with my testing database, where I drop and recreate the entire database with each testing run. I discovered that this is not as helpful in real-world usage when I tried to load design documents into my existing development database. So it is back into the code to add this functionality...

In RSpec parlance, the Store class should be able to put a new document. The Store.put class method is already able to put documents. What I need now is a possible destructive put or, in idiomatic ruby, Store.put!. The example that describes using this method for new documents:
    it "should be able to put a new document" do
Store.
should_receive(:put).
with("uri", { })

Store.put!("uri", { })
end
Thus starts the change-the-message or make-it-pass BDD cycle. First up, change the message in this:
cstrom@jaynestown:~/repos/couch_design_docs$ spec spec/couch_design_docs_spec.rb
.F.......

1)
NoMethodError in 'CouchDesignDocs::Store a valid store should be able to put a new document'
undefined method `put!' for CouchDesignDocs::Store:Class
./spec/couch_design_docs_spec.rb:30:

Finished in 0.010079 seconds

9 examples, 1 failure
To change that message, I add a put! method to the Store class:
    def self.put!
end
Another message to be changed:
cstrom@jaynestown:~/repos/couch_design_docs$ spec spec/couch_design_docs_spec.rb
.F.......

1)
ArgumentError in 'CouchDesignDocs::Store a valid store should be able to put a new document'
wrong number of arguments (2 for 0)
./spec/couch_design_docs_spec.rb:30:in `put!'
./spec/couch_design_docs_spec.rb:30:

Finished in 0.010416 seconds

9 examples, 1 failure
As the example describes, I need to be able to pass two arguments to this method, the URL of the document and the document itself:
    def self.put!(path, doc)
end
And now, I receive this failure:
cstrom@jaynestown:~/repos/couch_design_docs$ spec spec/couch_design_docs_spec.rb
.F.......

1)
Spec::Mocks::MockExpectationError in 'CouchDesignDocs::Store a valid store should be able to put a new document'
expected :put with ("uri", {}) once, but received it 0 times
./spec/couch_design_docs_spec.rb:26:

Finished in 0.009909 seconds

9 examples, 1 failure
At this point, I no longer need to change the message, but am ready to make the example pass:
    def self.put!(path, doc)
self.put(path, doc)
end
So far, there is no difference between put and put!. Let's make a difference. If the put fails, it should delete so that a subsequent put will succeed. Or, in RSpec form:
    it "should delete existing docs if first put fails" do
Store.
stub!(:put).
and_raise(RestClient::RequestFailed)

Store.should_receive(:delete)

Store.put!("uri", { })
end
To get that passing, I add a rescue block that deletes the existing document:
    def self.put!(path, doc)
self.put(path, doc)
rescue RestClient::RequestFailed
self.delete

end
Now I need to add another put call after the delete in the rescue block. This is a little tricky to describe with RSpec. Something like this does not work:
    it "should retry the put if the first fails" do
Store.
should_receive(:put).
exactly(:twice).
and_raise(RestClient::RequestFailed)

Store.stub!(:delete)

Store.put!("uri", { })
end
I try to implement this with a second put:
    def self.put!(path, doc)
self.put(path, doc)
rescue RestClient::RequestFailed
self.delete(path)
self.put(path, doc)
end
The problem with this is that both put calls now raise errors. The first is caught by the rescue block, but the second one is uncaught:
cstrom@jaynestown:~/repos/couch_design_docs$ spec spec/couch_design_docs_spec.rb
..FF.......

1)
RestClient::RequestFailed in 'CouchDesignDocs::Store a valid store should delete existing docs if first put fails'
HTTP status code
/home/cstrom/repos/couch_design_docs/lib/couch_design_docs/store.rb:24:in `put!'
./spec/couch_design_docs_spec.rb:42:

2)
RestClient::RequestFailed in 'CouchDesignDocs::Store a valid store should retry the put if the first fails'
HTTP status code
/home/cstrom/repos/couch_design_docs/lib/couch_design_docs/store.rb:24:in `put!'
./spec/couch_design_docs_spec.rb:53:

Finished in 0.010869 seconds

11 examples, 2 failures
It is tempting to look upon this as a limitation of RSpec. Maybe it is, but I take this as an opportunity to create a delete_and_put method:
    it "should be able to delete and put" do
Store.
should_receive(:delete).
with("uri", { })

Store.
should_receive(:put).
with("uri", { })

Store.delete_and_put("uri", { })
end
Making that example pass is a simple matter of moving the delete and put calls in the rescue block into a new delete_and_put method:
    def self.delete_and_put(path, doc)
self.delete(path)
self.put(path, doc)
end
I can then redo the earlier, should delete upon put failure example, to read:
    it "should delete existing docs if first put fails" do
Store.
stub!(:put).
and_raise(RestClient::RequestFailed)

Store.
should_receive(:delete_and_put).
with("uri", { })

Store.put!("uri", { })
end
The final implementation of the put! method then becomes:
    def self.put!(path, doc)
self.put(path, doc)
rescue RestClient::RequestFailed
self.delete_and_put(path, doc)
end
Nice! As I said, it was tempting to think of the difficulties with the two and_raise calls as a limitation of RSpec, but that "limitation" led a cleaner implementation.

After switching the Store.load code to use the new put! method, I give the gem another try on my development database:
cstrom@jaynestown:~/repos/eee-code$ irb
>> require 'couch_design_docs'
=> true
>> dir = CouchDesignDocs::Directory.new("/home/cstrom/repos/eee-code/couch/_design")
=> #<CouchDesignDocs::Directory:0xb79776bc @couch_view_dir="/home/cstrom/repos/eee-code/couch/_design">
>> store = CouchDesignDocs::Store.new("http://localhost:5984/eee")
=> #<CouchDesignDocs::Store:0xb797126c @url="http://localhost:5984/eee">
>> store.load(dir.to_hash)
=> {"lucene"=>{"transform"=>"function(doc) { … }"}}
Nice! The store.load call failed yesterday because of the put instead of the new put!.

Before calling it a day, I update the version number of my gem to 1.0.1 (I probably should have started below 1.0) by editing the lib/couch_design_docs.rb file created for me by Bones to include:
  VERSION = '1.0.1'
Then I regenerate my gemspec:
cstrom@jaynestown:~/repos/couch_design_docs$ rake gem:spec
(in /home/cstrom/repos/couch_design_docs)
(commit)

Up tomorrow: a convenience method so that I can upload design docs with a single call, and then I will replace the current code doing this in my application with said convenience method.

No comments:

Post a Comment