Thursday, July 9, 2009

Building and Merging Deep Hashes

‹prev | My Chain | next›

Today, I continue work on my couch_design_docs gem. The basic idea remains the same, a javascript file in couch/_design/lucene/transform.js should describe a CouchDB design document:
{
"lucene": {
"transform": <<contents of transform.js>>
}
}
So far, I have a Directory class that converts the directory path, file basename and file contents into an array. Next up, I need a way to convert arrays into deep hashes:
  it "should convert arrays into deep hashes" do
Directory.
a_to_hash(%w{a b c d}).
should == {
'a' => {
'b' => {
'c' => 'd'
}
}
}
end
Running the RSpec example, I get a failure:
cstrom@jaynestown:~/repos/couch_design_docs$ spec ./spec/couch_design_docs_spec.rb
.F.

1)
NoMethodError in 'CouchDesignDocs::Directory should convert arrays into deep hashes'
undefined method `a_to_hash' for CouchDesignDocs::Directory:Class
./spec/couch_design_docs_spec.rb:17:

Finished in 0.006896 seconds

3 examples, 1 failure
I change the message by defining an empty a_to_hash class method:
cstrom@jaynestown:~/repos/couch_design_docs$ spec ./spec/couch_design_docs_spec.rb 
.F.

1)
'CouchDesignDocs::Directory should convert arrays into deep hashes' FAILED
expected: {"a"=>{"b"=>{"c"=>"d"}}},
got: nil (using ==)
./spec/couch_design_docs_spec.rb:25:

Finished in 0.007203 seconds

3 examples, 1 failure
No more changing the message is needed, now I can make it pass. I still rather fancy tail recursion, so I end up with something similar to the other day:
    def self.a_to_hash(a)
key = a.first
if (a.length > 2)
{ key => a_to_hash(a[1,a.length]) }
else
{ key => a.last }
end
end
With that, I am ready to assemble all javascript files into a design docs structure. First up, I make sure that I have more than one javascript file in the fixtures directory:
cstrom@jaynestown:~/repos/couch_design_docs$ find fixtures/
fixtures/
fixtures/a
fixtures/a/b
fixtures/a/b/d.js
fixtures/a/b/c.js
Then I write my example to drive things along:
    it "should assemble all documents into a single docs structure" do
@it.to_hash.
should == {
'a' => {
'b' => {
'c' => 'function(doc) { return true; }',
'd' => 'function(doc) { return true; }'
}
}

}
end
The code that implements this iterates over each .js file in the design docs directory, using the previously built methods to build hashes to be merged together:
    def to_hash
Dir["#{couch_view_dir}/**/*.js"].inject({}) do |memo, filename|
hash = Directory.a_to_hash(expand_file(filename))
memo.merge(hash)
end
end
Simple enough, but wrong:
cstrom@jaynestown:~/repos/couch_design_docs$ spec ./spec/couch_design_docs_spec.rb 
...F

1)
'CouchDesignDocs::Directory a valid directory should assemble all documents into a single docs structure' FAILED
expected: {"a"=>{"b"=>{"c"=>"function(doc) { return true; }", "d"=>"function(doc) { return true; }"}}},
got: {"a"=>{"b"=>{"c"=>"function(doc) { return true; }"}}} (using ==)
./spec/couch_design_docs_spec.rb:48:

Finished in 0.008113 seconds

4 examples, 1 failure
The problem is the merge, which merges at the top level (the "a" key). I want the deepest hashes (the values of "b") to be merged together. Something like this does the trick:
    def deep_hash_merge(h1, h2)
h2.each_key do |k|
if h1.key? k
deep_hash_merge(h1[k], h2[k])
else
h1[k] = h2[k]
end
end
h1
end
It works, but it is not side-effect free. I will likely retry that bit of code tomorrow. My brain is not working so well at this point, so it is a good time to call it a day. Before I quit, leave myself a note:
    it "should assemble all documents into a single docs structure" do
pending "you can do a better job with deep hash merging than that"
@it.to_hash.
should == {
'a' => {
'b' => {
'c' => 'function(doc) { return true; }',
'd' => 'function(doc) { return true; }'
}
}

}
end
Hopefully my tomorrow self will not be too offended.

2 comments:

  1. This is really cool territory here. Have you seen the CouchApp framework for managing files -> JSON?

    http://github.com/couchapp/couchapp

    It's basically what you're building here but we've got macros and the ability to clone from a design document down to your filesystem.

    ReplyDelete
  2. Yah, I scanned through CouchApp before starting down this rabbit hole. I think the HTML in the design docs threw me. That and the Python. Well, mostly the Python ;)

    I will make an effort to look through it in more depth today to see if there are some more ideas in there that I might borrow.

    Thanks!

    ReplyDelete