Sunday, August 2, 2009

Generalizing to Handle Map and Reduce

‹prev | My Chain | next›

I left off work on my chain yesterday having recognized that Cucumber once again identified a bug in my code. The bug can be seen on the list of meals by month:

Specifically, the intra-month links are not the right dates (should be April 2005 and June 2005). Worse yet, they are not even links!

I can trace the trouble back to the last time that I worked on the helper method that links to previous/next date records in a CouchDB view. I claimed a mediocre solution then. It would seem that I have not even achieved that low mark.

The helper, link_to_adjacent_view_date requires a current key and a result set from a CouchDB view. It uses those inputs to determine the next record in the result set, yielding the next record back to the caller so that the caller, which knows the current context, can build the link.

The helper so far, complete with copious documentation and TODO indicative of my struggles with it:
    # TODO: use CouchDB view directly here, with limit=1 to determine
# the next record
def link_to_adjacent_view_date(current, couch_view, options={})
# If looking for the record previous to this one, then we seek a
# date prior to the current one - build a Proc capable of
# finding that
compare = options[:previous] ? { |date_fragment, current| date_fragment < current} : { |date_fragment, current| date_fragment > current}

# If looking for the record previous to this one, then we need
# to reverse the list before using the compare Proc to detect
# the record
next_result = couch_view.
send(options[:previous] ? :reverse : :map).
detect{|result| compare[result['key'], current.to_s]}

# If a next record was found, then return link text - either by
if next_result
if block_given?
yield next_result['value']
next_uri = next_result['key'].gsub(/-/, '/')
%Q|<a href="/meals/#{next_uri}">#{next_result['key']}</a>|
I am not particularly fond of pulling back then entire view result in order to accomplish this. It does work, though. It works for links between recipes, individual meals, and years. It just does not work for links between months.

The ultimate source of trouble is that I am trying to use this helper for both regular CouchDB view and reduced views. One size does not always fit all and this is a good example. It is also a good example of me not providing sufficient context in my specs, as I will show in a moment.

In the link_to_adjacent_view_date helper, things break down when yielding back to the Haml template to build the links. The value that is being yielded is:
if block_given?
yield next_result['value']
That works when the value is something like what can be found in regular (non-reduced) views:
cstrom@jaynestown:~/repos/eee-code$ curl \
{"id":"2005-04-20","key":"2005-04-20","value":{"title":"Tuna Casserole like Grammy's","date":"2005-04-20"}},
{"id":"2005-04-22","key":"2005-04-22","value":{"title":"Hot Lips: The Day After","date":"2005-04-22"}},
{"id":"2005-05-01","key":"2005-05-01","value":{"title":"Ode to the Farmers' Market","date":"2005-05-01"}},
{"id":"2005-05-02","key":"2005-05-02","value":{"title":"May Twoth","date":"2005-05-02"}},
Problems arise when the value contains less information, as with reduced views. The entire point of a reduce is to distill the information in the normal view down to some kind of summary representation, such as a count of the records with a particular key. This is what the count_by_month map-reduce view does:
cstrom@jaynestown:~/repos/eee-code$ curl \
Trying to build a link to a date with a value of "7" is not going to do much good. I could use the key in this case, but that is not enough information to link to meals and recipes. This is where the one-size fits all approach to maps and reduces breaks down.

I can resolve this by passing both the key and the value to the calling block. That will require some code changes, but mostly why did I miss this in the first place? The specs for the by-month version of the method:
describe "link_to_adjacent_view_date" do
context "couchdb view by_month" do
before(:each) do
@by_month = [{"key" => "2009-04", "value" => "foo"},
{"key" => "2009-05", "value" => "bar"}]
Gah! That's what I get for testing with data that does not accurately represent the live data.

So I change to more reduce-like data:
    before(:each) do
@by_month = [{"key" => "2009-04", "value" => "1"},
{"key" => "2009-05", "value" => "2"}]
And I update one of the block examples so that it can make use of the updated pre-condition:
    it "should link to the CouchDB view's key and value, if block is given" do
link_to_adjacent_view_date("2009-04", @by_month) do |key, value|
%Q|<a href="/foo">#{key}</a>|
should have_selector("a",
:href => "/foo",
:content => "2009-05")
Now the example fails like it ought to:
'link_to_adjacent_view_date couchdb view by_month should link to the CouchDB view's key and value, if block is given' FAILED
expected following output to contain a <a href='/foo'>2009-05</a> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "">
<html><body><a href="/foo">2</a></body></html>

Finished in 0.106256 seconds

60 examples, 1 failure
It fails because the link_to_adjacent_view_date helper is not yielding both the key and value needed in the example. Adding the key makes the example pass:
if block_given?
yield next_result['key'], next_result['value']
Of course that breaks just about every one of my view examples, but they are easy to fix—I just need to update the Haml views to work with two values being yielded by link_to_adjacent_view_date, instead of 1.

With that, I finally have all of my specs passing and all of my Cucumber scenarios:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -i
32 scenarios (7 undefined, 25 passed)
272 steps (22 skipped, 24 undefined, 226 passed)

No comments:

Post a Comment