Monday, October 26, 2009

"Newbie" Feedback

I recently had the privilege to supply the challenge for the second ever Ruby Programming Challenge For Newbies. I thought it pretty cool that the challenge provoked 40+ "newbies" to submit responses. As one might expect, there was some rough Ruby, but very few butcherings of the language.

In the spirit of the code review, I tried to provide constructive feedback to all participants. Following is a summary of some common suggestions that I had to offer...

Bang Methods

Method parameters are mutable in Ruby. If you modify the value of the parameter inside a method, the original value changes. This can lead to all sorts of unexpected side-effects. There are some exceptions to this, but it is best to take no chances and treat it as a rule.

This can be a bit frightening at first, but Ruby has a convention of adding a bang (!) to the end of a method name when side-effects are expected. Many of the core methods follow this convention. For instance consider the difference between map and map!:
a = [1, 2, 3]

# Double every value in the array
>> a.map{|x| x * 2}
=> [2, 4, 6]

# The original value has not changed
>> a
=> [1, 2, 3]

# Double every value in the array with a bang method
>> a.map!{|x| x * 2}
=> [2, 4, 6]

# The original value is changed
>> a
=> [2, 4, 6]
The problem requested a solution with a method signature of average_time_of_day(times), where the times parameter was list of strings (e.g. %w{6:41am 6:51am 7:01am}). Since the method does not end with a bang, it should not modify the input (e.g. by calling a bang method like map!). A third of the submissions modified the times array inside that method—or worse, in other methods that were called by average_time_of_day.

Since average_time_of_day was the entirety of the challenge, modifying the input was not a huge problem. Still, not a good habit to start.

Enumerable

Since the challenge involved averaging times, most solutions involved summing up these times. A typical solution looked something like this:
sum = 0
i = 0
while i < times.length
time = times[i]
# parse / manipulate the time
sum = sum + time
i = i + 1
end
It is nice that Ruby provides syntax like for and while, making the transition from other languages so easy. There is a cleaner way to accomplish this in Ruby:
sum = 0
times.each do |time|
# parse / manipulate the time
parsed_time = #....
sum = sum + parsed_time
end
Eschewing while in favor of an each block eliminates the need for the tracking variable, i. With the assignment and computation of the tracking variable eliminated, there is less to distract from the actual intent of the code. Less code makes intent clearer.

The only thing still getting in the way of intent is the accumulator variable, sum. It is being initialized outside of the each block and manipulated inside the block. If you think of a block like a function, then this is akin to modifying a global variable inside a function.

Ruby has an inject method (also known as reduce and fold in Ruby 1.8.7+) to handle this:
sum = times.inject(0) do |accumulator, time|
# Manipulate the time somehow
accumulator + time
end
An inject starts with a seed value for an accumulator (sometimes called a memo). As inject iterates over each value, the last value in the block is used to seed the accumulator the next iteration. The end result of the iteration is the accumulation of each of these values. In the case of the code snippet above, it is the summation of the times.

But wait, there's more! It is possible to compact this even further...

Chaining

If you are not using bang methods, then you can chain iterators together. Let's say your solution involved mapping time strings to time objects and then sorting them, this can be accomplished via:
def average_time_of_day(times)
sorted_times = times.
map { |t| Time.parse(t) }.
sort
# Do more stuff ...
end
The sorted_times local variable now contains, well... sorted times. The time strings were mapped to actual time objects, which could then be sorted. That's just lovely!

Going back to the accumulator example, it is possible to chain together iterator to make intent crystal clear:
sum = times.
map { |str| Time.parse(str) }.
inject { |sum, t| sum + t }
It is possible to achieve even greater clarity through symbol to proc shorthand:
class String
def to_time
Time.parse(self)
end
end

def average_time_of_day(times)
sum = times.map(&:to_time).inject(&:+)
# Calculate the average ...
end
The theory behind symbol-to-proc is probably a bit beyond "newbie" scope, but still worth seeing to give an idea how close to the domain Ruby allows you to get. In that last code snippet, I am taking the times strings, mapping them to Time instances (with a monkey patch to String) and adding them all together. Intent does not get much clearer than that.

Read the documentation on Ruby's Enumerable module. Even seasoned Rubyists can benefit from re-reading it regularly.

Test Harness

Some will claim that the Ruby community is too "test obsessed". There are very good reasons to embrace TATFT / BDD, etc. I could write for months on why this is a good idea. In fact I did. But this is not one of those times.

Using a test harness for an algorithm like average_time_of_day makes too much sense not to use it. Many of the submissions were clearly iterating toward a solution. The author got it working for the simple case, then tried to get it working crossing midnight—only to break the simple case. Many authors never noticed that the simple case was no longer working and submitted half working solutions.

As soon as you have a simple solution working, write a test (if not sooner!):
def average_time_of_day(times)
# incredibly simplistic solution
end

require 'test/unit'

class TestAverageTimeOfDay < Test::Unit::TestCase
def test_simple_times
assert_equal('6:51am', average_time_of_day(%w{6:41am 6:51am 7:01am}))
end
end
If you had saved this file as average_time_of_day.rb, you could execute this test after each change to the average_time_of_day method thusly:
jaynestown% ruby average_time_of_day.rb
Loaded suite average_time_of_day
Started
.

1 test, 1 assertion, 0 failures, 0 errors
Ruthlessly running this command as the average_time_of_day method evolves will tell you immediately when something is broken. The sooner you know something is wrong, the sooner you can address it.

Don't test because a segment of the Ruby community forces you to, do it because it is so damn easy there is no reason not do.

Conclusion

That about sums up my feedback to the "newbies". I really did enjoy providing the challenge and very much appreciated so many quality solutions. I hope most of you continue your learning and I look forward to seeing some of your names driving the future of Ruby!

Friday, October 16, 2009

Smoke 'em If You Got 'em

‹prev | My Chain | next›

I did a quick push to the beta site this morning so that I could do some smoke testing. For the most part, everything looked good. Sadly, I did discover another oversight in my switch of the recipe URL delimiter from dashes to slashes. The recipe search results still had the dashes in them.

I am somewhat disappointed that my Cucumber scenarios did not catch this error. It would have been a simple matter of one of the 10 recipe search scenarios following a link from the search results to the actual recipe. A simple matter indeed, but I did not include such a step.

This is, of course, the point of smoke tests. Even if you have awesome tests, even if you do not make big changes the day before deploying, it is always a good idea to poke around. There may be a bit of smoke, there may be a raging inferno, but you won't know unless you exercise the code.

After fixing the link in the search results (a minor code change, but involving many spec changes), I am ready to deploy. But first...

I am not certain that I have identified the last of the dash/slash issues. Rather than delay the promotion to production, I do a bit of defensive Rack configuration with Rack::Rewrite. Specifically, a recipe URL that contains dashes should be redirected to the proper slash URL:
use Rack::Rewrite do
r301 %r{(.+)\.html}, '$1'
r301 %r{^/recipes/(\d{4})-(\d{2})-(\d{2})-(.+)}, '/recipes/$1/$2/$3/$4'
end
The curl command verifies that this works:
jaynestown% curl -I http://localhost:3000/recipes/2008-07-13-sausage   
HTTP/1.1 301 Moved Permanently
Location: /recipes/2008/07/13/sausage
Content-Length: 14
Connection: keep-alive
Server: thin 1.2.2 codename I Find Your Lack of Sauce Disturbing
With all of my RSpec tests passing, all of my Cucumber scenarios passing, and a bit of defensive Rack middleware, I am ready to deploy my Sinatra / CouchDB application:
jaynestown% rake vlad:stop_app
jaynestown% rake vlad:update vlad:migrate vlad:start_app
Finally, I update DNS so that http://legacy.eeecooks.com points to the legacy Rails site and http://www.eeecooks.com points to the new Sinatra / CouchDB site.

Thus endeth the chain.

Thursday, October 15, 2009

A Slight Rewrite

‹prev | My Chain | next›

As mentioned yesterday, my last minute decision to switch the dates in recipe URLs from dash separated (/recipes/YYYY-MM-DD-short_name) to slash separated (/recipes/YYYY/MM/DD/short_name) caused many things to break. My specs / unit tests caught a few of these failures. My Cucumber scenarios caught many. I am perfectly comfortable with this.

My specs drive design. Through behavior driven development, they ensure that my design is the simplest thing that can possibly work. Simple means fewer bugs today and less legacy tomorrow. And, hey, if they happen to catch a bug or two, that's great. Specs are not for regression testing, they remain to reflect my thought process when I return months (years) later.

This is not to say that regression testing is not important—it is. However, regression testing is not as valuable as clean design is for long term health of a project.

One of the aspects of Cucumber that I value so much is that it can drive features, but also verify them once they are implemented. As such, I rely heavily on Cucumber to catch regressions.

In the case of my change yesterday, I ended up with 10 regressions identified by Cucumber. I am able to fix all of them by updating visit statements in the Cucumber steps and updating a few helpers:
jaynestown% cucumber features
...
39 scenarios (1 pending, 38 passed)
344 steps (1 pending, 343 passed)
0m42.922s
One last thing for me to do is to accommodate really old URLs. When we first put up the site, we baked HTML from XML. Thus all of the URLs had .html extensions on them. The legacy Rails site supported extensions, but linked internally without the extension. But what if someone has a really old bookmark? I would prefer not to dump them onto the Not Found page and, thanks to John Trupiano's Rack::Rewrite, I do not have to worry.

After gem installing rack-rewrite, I add this to my rackup file:
require 'rack-rewrite'

###
# Rewrite
use Rack::Rewrite do
r301 %r{(.+)\.html}, '$1'
end
Now, when I ask for a sausage recipe with the .html extension, I am redirected to its new, permanent location:
jaynestown% curl -I http://localhost:3000/recipes/2008/07/13/sausage.html
HTTP/1.1 301 Moved Permanently
Location: /recipes/2008/07/13/sausage
Content-Length: 14
Connection: keep-alive
Server: thin 1.2.2 codename I Find Your Lack of Sauce Disturbing
Tomorrow I will deploy and close out my chain.

Wednesday, October 14, 2009

Regression Testing

‹prev | My Chain | next›

Gah! A discussion at B'more on Rails got me to thinking. If I am replacing my old site, will old URLs still work? It is an important question to answer as Google and others will have links to the legacy URLs. I would like to make the transition as easy as possible on my users.

I was pretty consistent with my URLs. This is the third time that I have implemented the same site, so the URLs are somewhat ingrained by now. But...
get '/recipes/:permalink' do
data = RestClient.get "#{@@db}/#{params[:permalink]}"
#...
end
The :permalink in the URL is being passed thru to CouchDB. The IDs in CouchDB are of the form YYYY-MM-DD-short_name. Thus, to pull back a recipe, I would need to access a URL in the Sinatra app of the form /recipes/YYYY-MM-DD-short_name.

That all works well. It is fully tested and even verified with Cucumber. The problem? The legacy site links to URLs like /recipes/YYYY/MM/DD/short_name. Slightly less bothersome, the rest of the Sinatra app is consistent using slashes rather than dashes. So, to keep consistent with the rest of the site and the legacy site, I need to switch the Sinatra app to:
get %r{/recipes/(\d+)/(\d+)/(\d+)/?(.*)} do |year, month, day, short_name|
data = RestClient.get "#{@@db}/#{year}-#{month}-#{day}-#{short_name}"
#...
end
I do not know if it is hubris, but I like to make these kinds of changes without testing first. I already have excellent test coverage, I expect those tests to identify problems in the code caused by such a change. Put another way, this is already covered by tests, I just need to update the expectations to align with a new reality.

Running all of my specs, I find two such failures:
jaynestown% spec ./spec/eee_spec.rb
................FF.....................................

1)
Spec::Mocks::MockExpectationError in 'eee cached documents GET /recipes/permalink should etag with the CouchDB document's revision'
RestClient expected :get with (any args) once, but received it 0 times
./spec/eee_spec.rb:243:

2)
'eee a CouchDB recipe GET /recipes/permalink should respond OK' FAILED
expected ok? to return true, got false
./spec/eee_spec.rb:275:

Finished in 1.50 seconds
Both are easily fixed by changing the expectation to honor the new slash URLs.

So is such a change just that easy? Not quite.

Tests / examples are useful for driving design. They help to ensure that I implement the simplest thing that could possibly work. This, in turn, allows for future growth. Because I implement the simplest thing that can possibly work, I am all but certain to be adhering to YAGNI (You Ain't Gonna Need It). In other words, I am not painting myself into a corner designing some super elegant architecture only find 3 months later that I need to remove it because it is causing problems or worse, preventing from moving in a different, previously unanticipated direction.

So tests / examples are invaluable to me, but they do not do a good job of finding bugs. So what about regression testing? Well, that is what Cucumber's full stack execution is great for. And right now, I have 12 failing scenarios where once I had all passing:

jaynestown% cucumber features
...
Failing Scenarios:
cucumber features/recipe_replacement.feature:14 # Scenario: A previous version of the recipe
cucumber features/recipe_details.feature:7 # Scenario: Viewing a recipe with several ingredients
cucumber features/recipe_details.feature:15 # Scenario: Viewing a recipe with non-active prep time
cucumber features/recipe_details.feature:22 # Scenario: Viewing a list of tools used to prepare the recipe
cucumber features/recipe_details.feature:28 # Scenario: Main site categories
cucumber features/recipe_details.feature:35 # Scenario: Viewing summary and recipe instructions
cucumber features/recipe_details.feature:42 # Scenario: Navigating to other recipes
cucumber features/recipe_alternate_preparations.feature:14 # Scenario: Alternate preparation
cucumber features/draft_recipes.feature:7 # Scenario: Navigating between recipes
cucumber features/browse_meals.feature:36 # Scenario: Browsing a meal on a specific date
cucumber features/site.feature:7 # Scenario: Quickly scanning meals and recipes accessible from the home page
cucumber features/site.feature:64 # Scenario: Send compliments to the chef on a delicious recipe

39 scenarios (12 failed, 1 pending, 26 passed)
344 steps (12 failed, 37 skipped, 1 pending, 294 passed)
0m40.568s
The problem is twofold: some Cucumber steps are visiting recipe URLs with slashes and I have helpers that are still generating links to recipes using the slashes. The former is easy enough to address. An example of the latter is this failure:
jaynestown% cucumber features/browse_meals.feature:36
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Browse Meals

So that I can find meals made on special occasions
As a person interested in exploring meals and how they drive certain recipes
I want to browse meals by date

Scenario: Browsing a meal on a specific date # features/browse_meals.feature:36
Given a "Focaccia" recipe from March 3, 2009 # features/step_definitions/recipe_details.rb:137
Given a "Focaccia! The Dinner" meal with the "Focaccia" recipe on the menu # features/step_definitions/meal_details.rb:23
When I view the "Focaccia! The Dinner" meal # features/step_definitions/meal_details.rb:54
Then I should see the "Focaccia! The Dinner" title # features/step_definitions/meal_details.rb:71
And I should see a "Focaccia" recipe link in the menu # features/step_definitions/meal_details.rb:91
When I click the "March" link # features/step_definitions/meal_details.rb:63
Then I should see "Focaccia! The Dinner" in the list of meals # features/step_definitions/meal_details.rb:103
When I click the "Focaccia! The Dinner" link # features/step_definitions/meal_details.rb:63
And I click the "2009" link # features/step_definitions/meal_details.rb:63
Then I should see "Focaccia! The Dinner" in the list of meals # features/step_definitions/meal_details.rb:103
When I click the "Focaccia! The Dinner" link # features/step_definitions/meal_details.rb:63
When I click the "Focaccia" link # features/step_definitions/meal_details.rb:63
Then I should see the "Focaccia" recipe # features/step_definitions/meal_details.rb:107
expected following output to contain a <h1>Focaccia</h1> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>EEE Cooks</title>
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css">
<link href="main.rss" rel="alternate" title="EEE Cooks RSS" type="application/rss+xml">
<link href="recipes.rss" rel="alternate" title="EEE Cooks Recipe RSS" type="application/rss+xml">
</head>
<html><body>
<div id="header">
<div id="eee-header-logo">
<a href="/">
<img alt="Home" src="/images/eee_corner.png"></a>
</div>
</div>
<ul id="eee-categories">
<li><a href="/recipes/search?q=category:italian">Italian</a></li>
...
<h1>
Not Found
</h1>

...
The link from the meal includes the slashes. When Cucumber tries to follow that link, the result is a page not found.

A gsub in the recipe_link helper addresses that problem:
    def recipe_link(link, title=nil)
permalink = link.gsub(/\//, '-')
recipe = JSON.parse(RestClient.get("#{_db}/#{permalink}"))
%Q|<a href="/recipes/#{recipe['_id'].gsub(/-/, '/')}">#{title || recipe['title']}</a>|
end
After updating the spec to align with the new reality of the code, I have that scenario passing as well as one other:
jaynestown% cucumber features                         
...
Failing Scenarios:
cucumber features/recipe_replacement.feature:14 # Scenario: A previous version of the recipe
cucumber features/recipe_details.feature:7 # Scenario: Viewing a recipe with several ingredients
cucumber features/recipe_details.feature:15 # Scenario: Viewing a recipe with non-active prep time
cucumber features/recipe_details.feature:22 # Scenario: Viewing a list of tools used to prepare the recipe
cucumber features/recipe_details.feature:28 # Scenario: Main site categories
cucumber features/recipe_details.feature:35 # Scenario: Viewing summary and recipe instructions
cucumber features/recipe_details.feature:42 # Scenario: Navigating to other recipes
cucumber features/recipe_alternate_preparations.feature:14 # Scenario: Alternate preparation
cucumber features/draft_recipes.feature:7 # Scenario: Navigating between recipes
cucumber features/site.feature:64 # Scenario: Send compliments to the chef on a delicious recipe

39 scenarios (10 failed, 1 pending, 28 passed)
344 steps (10 failed, 27 skipped, 1 pending, 306 passed)
0m41.200s
(commit)

Most, if not all, of the remaining failures are caused by bad Cucumber visit steps. I will address those tomorrow. Then I will be done with my chain.

Tuesday, October 13, 2009

404 in Sinatra

‹prev | My Chain | next›

Before going live, we need legit 404 and 500 error messages. In production mode, the default Sinatra error messages are a little unhelpful:



Sinatra provides two code blocks for handling errors:
not_found do
end

error do
end
I like having my not found / error templates to be named 404 / 500. Since I am using Haml, I can use the 404.haml and 500.haml templates:
not_found do
haml :'404'
end

error do
haml :'500'
end
I use :'404' because the haml method requires a symbol and :404 is not a valid symbol. After defining the contents of the Haml templates, I have my 404 and 500 error pages.

The only other thing that I do tonight is define the kid nicknames document. We share a lot of personal information about our family in the cookbook, but never the actual kids' names (we're even a little vague about birthdays). Maybe we're paranoid, but...

I have a helper that pulls the nicknames from CouchDB:
    def kid_nicknames
@@kid_kicknames ||= JSON.parse(RestClient.get("#{_db}/kids"))
end
Those nicknames are then used as part of the "wiki" method:
    def wiki(original, convert_textile=true)
text = (original || '').dup
#...
text.gsub!(/\[kid:(\w+)\]/m) { |kid| kid_nicknames[$1] }
#...
end
The seed data for the kids kids document has been empty. I define it:
{
"___":"our daughter",
"___":"our son",
"___":"the baby"
}
And then use my couch_docs gem to load into the CouchDB database:
jaynestown% couch-docs load . http://localhost:5984/eee
With that, I have the nicknames displaying correctly:



That will do for tonight. Tomorrow, I deploy the 404/500 and seed data to beta. Then I can switch beta to be the production site, thus ending my chain.

Monday, October 12, 2009

Deploying ImageScience to Debian

‹prev | My Chain | next›

After cleaning up a few minor issues, I have all of my RSpec examples passing:
jaynestown% rake                                      
(in /home/cstrom/repos/eee-code)

==
Sinatra app spec
.......................................................

Finished in 1.89 seconds

55 examples, 0 failures

==
Helper specs
..........................................................................................

Finished in 0.13 seconds

90 examples, 0 failures

==
View specs
..............................................................................................................

Finished in 1.37 seconds

110 examples, 0 failures
All of the Cucumber scenarios are passing as well:

jaynestown% cucumber features
...
39 scenarios (1 pending, 38 passed)
344 steps (1 pending, 343 passed)
0m43.335s
I have my various Rack middlewares configured so I think I am ready to deploy to the beta site:
jaynestown% rake vlad:stop_app
jaynestown% rake vlad:update vlad:migrate vlad:start_app
I run into a small problem with ImageScience and RubyInline. Specifically, I see errors similar to:
/home/cstrom/.ruby_inline/Inline_ImageScience_cdab.c:2:23: error: FreeImage.h: No such file or directory
/home/cstrom/.ruby_inline/Inline_ImageScience_cdab.c: In function ‘unload’:
/home/cstrom/.ruby_inline/Inline_ImageScience_cdab.c:8: error: ‘FIBITMAP’ undeclared (first use in this function)
/home/cstrom/.ruby_inline/Inline_ImageScience_cdab.c:8: error: (Each undeclared identifier is reported only once
...
I ultimately track that down to a missed system level package. I use Ubuntu for my development box, but Debian for the deployment server. On the former, the freeimage development packages was libfreeimage3-dev, but on Debian it should be libfreeimage-dev (without the 3). I had missed the no-such package warning in the apt-get output. No doubt this is the kind of thing that Chef would address.

At any rate, I now have the thumbnailing site up and running on the beta site, and the images are nice and small on the homepage:



I still need a proper 404 page and one last bit of seed data. Then I am done with my chain.

Sunday, October 11, 2009

Rack::ThumbNailer

‹prev | My Chain | next›

As of yesterday, I have my Rack middleware thumbnail-ing application images when the request has the thumbnail parameter.

I am using ImageScience (documentation), which does all of its work in the filesystem. Since thumbnails are being stored on the filesystem, I might as well keep them there and serve them up whenever subsequent requests come in for the same resource.

Describing this in RSpec:
      context "and a previously generated thumbnail" do
before(:each) do
File.stub!(:exists?).and_return(true)
end
it "should not make a new thumbnail" do
Rack::ThumbNailer.
should_not_receive(:mk_thumbnail)
get "/foo.jpg", :thumbnail => 1
end
end
This fails because nothing is preventing the call to mk_thumbnailer:
1)
Spec::Mocks::MockExpectationError in 'ThumbNailer Accessing an image with thumbnail param and a previously generated thumbnail should not make a new thumbnail'
expected :mk_thumbnail with ("/var/cache/rack/thumbnails/foo.jpg", "image data") 0 times, but received it once
./thumbnailer.rb:18:in `call'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/mock_session.rb:30:in `request'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/test.rb:207:in `process_request'
/home/cstrom/.gem/ruby/1.8/gems/rack-test-0.5.0/lib/rack/test.rb:57:in `get'
./thumbnailer_spec.rb:67:
To make that example pass, I add an unless File.exists? to the call Rack method:
    def call(env)
req = Rack::Request.new(env)
if !req.params['thumbnail'].blank?
filename = @options[:cache_dir] + req.path_info

unless ::File.exists?(filename)
image = ThumbNailer.rack_image(@app, env)
ThumbNailer.mk_thumbnail(filename, image)
end

thumbnail = ::File.new(filename).read
[200, { }, thumbnail]
else
@app.call(env)
end
end
That should just about do it. I add it to my config.ru Rackup file:
...
###
# Thumbnail

require 'rack/thumbnailer'
use Rack::ThumbNailer,
:cache_dir => '/tmp/rack/thumbnails'

###
# Sinatra App
...
Trying this out for real, I access a real image:



And, when I add the thumbnail query parameter:



Nice!

Now that I have my Rack middleware, I need to use it. I have designed it such that the image needs to be called with a query parameter. Normally, a query parameter on an image request will have no effect. Thus, opting for a query parameter seemed like a nice way of keeping my Sinatra app somewhat de-coupled from the Rack middleware. If the middleware is present a thumbnail will be returned. If the Rack middleware is not present, the full-size image will be returned (possibly with width and height attributes scaling the image in the browser).

The homepage currently has meal thumbnails included like:
...
%a{:href => date.strftime("/meals/%Y/%m/%d")}
= (image_link meal, :width => 200, :height => 150)
%h2
%a{:href => date.strftime("/meals/%Y/%m/%d")}= meal["title"]
...
Currently the image_link helper is returning an image tag that tells the browser to scale the image (something like <img src="/images/foo.jpg" width="200" height="150">). I would like to be able to pass additional query parameters to the image_link helper. I describe this in RSpec as:
describe "image_link" do
it "should include query parameters" do
image_link(@doc, { }, :foo => 1).
should have_selector("img",
:src => "/images/#{@doc['_id']}/sample.jpg?foo=1")
end
end
At first that fails with an incorrect number of arguments message:
1)
ArgumentError in 'image_link a document with an image attachment should include query parameters'
wrong number of arguments (3 for 2)
./spec/eee_helpers_spec.rb:202:in `image_link'
./spec/eee_helpers_spec.rb:202:
I change the message by adding an additional, optional third argument to the helper:
    def image_link(doc, options={ }, query_params={ })
# ...
end
I still get a failure because the expectation is not met:
1)
'image_link a document with an image attachment should include query parameters' FAILED
expected following output to contain a <img src='/images/foo/sample.jpg?foo=1'/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><img src="/images/foo/sample.jpg"></body></html>
./spec/eee_helpers_spec.rb:202:
To make it pass, I rely on Rack::Utils.build_query to assemble the query string from a hash:
    def image_link(doc, options={ }, query_params={ })
#...
attrs = options.map{|kv| %Q|#{kv.first}="#{kv.last}"|}.join(" ")
query = query_params.empty? ? "" : "?" + Rack::Utils.build_query(query_params)
%Q|<img #{attrs} src="/images/#{doc['_id']}/#{filename}"/>|
end
With that passing, I update the homepage to trigger a thumbnail, if the Rack middleware is present:
          %a{:href => date.strftime("/meals/%Y/%m/%d")}
= (image_link meal, {:width => 200, :height => 150}, {:thumbnail => 1})
%h2
%a{:href => date.strftime("/meals/%Y/%m/%d")}= meal["title"]
And, checking it out in Firebug, the load time for my images is indeed down from 1-2 seconds all the way to ~100ms:



I will get that deployed to the beta site tomorrow and then... I think my chain may be done!