Sunday, March 7, 2010

DRY Design Docs

‹prev | My Chain | next›

As of yesterday, my couch_docs gem now supports the !code macro (which comes from couchapp). This allows me to DRY up my design documents (CouchDB map-reduces and show/list functions).

The couch_docs gem assembles CouchDB design documents from the filesystem—directories are keys in the design document and files are the values. This lets me work with .js files rather than functions inside of JSON documents (which is where the Javascript functions reside on the CouchDB server). The couch_docs gem expects to find design documents in a directory named _design. As of yesterday, it expects to find "library" files for DRYing up these design documents in _design/__lib.

But what of the __lib files on the server? Do I exclude the before uploading? I think not. They should not cause any harm on the CouchDB server unless they were accessed and I think the double underscore in the name ought to be sufficient to indicate that "Hey! These aren't going to do anything if you access them!" Uploading to the server has the added benefit of making them dump-able later—with no changes to the current code.

Ooooh. That bring up an interesting point though. On the CouchDB server, the !code macros have been evaluated, leaving them very much un-DRY. If I dump them, they ought to go back to being DRY. To accomplish that, I will need to delineate the expanded !code when uploading it to CouchDB so that I can replace it with !code macros when dumping them.

Since I am using functional / full stack RSpec example for this bit of code, I can simply update my expecations:
    it "should process code macros when assembling" do
@it.to_hash['x'].
should == {
'z' =>
"// !begin code foo.js\n" +
"function foo () { return \"foo\"; }\n" +
"// !end code foo.js\n" +
"function bar () { return \"bar\"; }\n"
}
end
That example fails, but is easy enough to get passing:
    def process_code_macro(line)
if line =~ %r{\s*//\s*!code\s*(\S+)\s*}
"// !begin code #{$1}\n" +
read_from_lib($1)
"// !end code #{$1}\n"
else
line
end
end
Now that I denote where the !code macros were evaluated when uploading design documents, how to de-evaluate them when dumping design documents to the file system? I would like it to work something like:
    it "should strip lib code when dumping" do
js = <<_JS
// !begin code foo.js
function foo () { return 'foo'; }
// !end code foo.js
// !begin code bar.js
function bar () { return 'bar'; }
// !end code bar.js
function baz () { return 'baz'; }
_JS

@it.
remove_code_macros(js).
should == "// !code foo.js\n" +
"// !code bar.js\n" +
"function baz () { return 'baz'; }\n"
end
There is relatively little change-the-message/make-it-pass to be done here. I need the remove_code_macros method to remove a code fragment, call itself again, remove another code fragment and keep doing so until there are no more !code fragments to be removed. I accomplish that will some regular expression fun and, of course a little recursion:
    def remove_code_macros(js)
js =~ %r{// !begin code ([.\w]+)$}m
lib = $1
if lib and js =~ %r{// !end code #{lib}$}m
remove_code_macros(js.sub(%r{// !begin code #{lib}.+// !end code #{lib}}m, "// !code #{lib}"))
else
js
end
end
With that, I should have an end-to-end mechanism for keeping my CouchDB design documents DRY. I will try this out with real design documents tomorrow to make sure I have not overlooked anything. Then it is onto the next item in my couch_docs 1.1 TODO list.

Day #35

1 comment:

  1. Thanks for blogging this. It looks really cool!

    When writing CouchApp, we've been very careful to take portability into account. Hence the support for both `couchapp push` and `couchapp clone`. I'm glad to see you decided to keep the lib directory in the design document. This means if someone else clones your app, and pushes it, they should be able to edit the code and run it again.

    ReplyDelete