Wednesday, September 30, 2009

Outside-In Ingredients

‹prev | My Chain | next›

Running the Cucumber scenario from yesterday, I find that all of the steps are currently undefined:
jaynestown% cucumber features/ingredient_index.feature:7 -s
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Ingredient index for recipes

As a user curious about ingredients or recipes
I want to see a list of ingredients
So that I can see a sample of recipes in the cookbook using a particular ingredient

Scenario: A couple of recipes sharing an ingredient
Given a "Cookie" recipe with "butter" and "chocolate chips"
And a "Pancake" recipe with "flour" and "chocolate chips"
When I visit the ingredients page
Then I should see the "chocolate chips" ingredient
And "chocolate chips" recipes should include "Cookie" and "Pancake"
And I should see the "flour" ingredient
And "flour" recipes should include only "Pancake"

1 scenario (1 undefined)
7 steps (7 undefined)
0m0.540s

You can implement step definitions for undefined steps with these snippets:

Given /^a "([^\"]*)" recipe with "([^\"]*)" and "([^\"]*)"$/ do |arg1, arg2, arg3|
pending
end

When /^I visit the ingredients page$/ do
pending
end

...
The first two steps in that scenario can be defined with:
Given /^a "([^\"]*)" recipe with "([^\"]*)" and "([^\"]*)"$/ do |title, ing1, ing2|
date = Date.new(2009, 9, 30)
permalink = date.to_s + "-" + title.downcase.gsub(/\W/, '-')

recipe = {
:title => title,
:type => 'Recipe',
:published => true,
:date => date,
:preparations => [{'ingredient' => {'name' => ing1}},
{'ingredient' => {'name' => ing2}}]
}

RestClient.put "#{@@db}/#{permalink}",
recipe.to_json,
:content_type => 'application/json'
end
Nothing too difficult in there—build a hash describing the recipe and then putting into the CouchDB database.

Next, I define the step to visit the ingredient index page that I created yesterday:
When /^I visit the ingredients page$/ do
visit "/ingredients"
end
Easy enough. Except:
jaynestown% cucumber features/ingredient_index.feature
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Ingredient index for recipes

As a user curious about ingredients or recipes
I want to see a list of ingredients
So that I can see a sample of recipes in the cookbook using a particular ingredient

Scenario: A couple of recipes sharing an ingredient # features/ingredient_index.feature:7
Given a "Cookie" recipe with "butter" and "chocolate chips" # features/step_definitions/ingredient_index.rb:1
And a "Pancake" recipe with "flour" and "chocolate chips" # features/step_definitions/ingredient_index.rb:1
When I visit the ingredients page # features/step_definitions/ingredient_index.rb:22
Resource not found (RestClient::ResourceNotFound)
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
./features/support/../../eee.rb:227:in `GET /ingredients'
(eval):7:in `get'
features/ingredient_index.feature:11:in `When I visit the ingredients page'
Then I should see the "chocolate chips" ingredient # features/ingredient_index.feature:12
And "chocolate chips" recipes should include "Cookie" and "Pancake" # features/ingredient_index.feature:13
And I should see the "flour" ingredient # features/ingredient_index.feature:14
And "flour" recipes should include only "Pancake" # features/ingredient_index.feature:15
Ah, the RestClient::ResourceNotFound exception is being raised because I have not put the CouchDB map/reduce required by the Sinatra action:
get '/ingredients' do
url = "#{@@db}/_design/recipes/_view/by_ingredients?group=true"
data = RestClient.get url
@ingredients = JSON.parse(data)['rows']

"<title>EEE Cooks: Ingredient Index</title>"
end
I am using the couch_docs gem to load design documents into the CouchDB test database, so all I need to do is create couch/_design/recipes/views/by_ingredients/map.js:
function (doc) {
if (doc['published']) {
for (var i in doc['preparations']) {
var ingredient = doc['preparations'][i]['ingredient']['name'];
var value = [doc['_id'], doc['title']];
emit(ingredient, {"id":doc['_id'],"title":doc['title']});
}
}
}
And couch/_design/recipes/views/by_ingredients/reduce.js:
function(keys, values, rereduce) {
if (rereduce) {
var ret = [];
for (var i=0; i<values.length; i++) {
ret = ret.concat(values[i]);
}
return ret;
}
else {
return values;
}
}
With that, I have the first three steps in this scenario passing. That means it is time to work my way into the code to start implementing the next step. The Sinatra action is already done (yesterday), so it is time to start on the Haml template:
describe "ingredients.haml" do
#...
end
From my work the other night, I know that the CouchDB view will return an ingredient list of the form:
  before(:each) do
@ingredients = [{'butter' =>
[
['recipe-id-1', 'title 1'],
['recipe-id-2', 'title 2']
]
},
{'sugar' =>
[
['recipe-id-2', 'title 2']
]
}]
end
Given that, I expect to find a list of two ingredients:
  it "should have a list fo ingredients" do
render("/views/ingredients.haml")
response.should have_selector("p .ingredient", :count => 2)
end
I can get that to pass with:
= @ingredients.each do |ingredient|
%p
%span.ingredient
= ingredient.keys.first
Last up tonight, I would like to include the recipe titles in the Haml output:
  it "should have a list of recipes using the ingredients" do
render("/views/ingredients.haml")
response.should have_selector("p", :content => 'title 1, title 2')
end
I can get that passing with:
= @ingredients.each do |ingredient|
%p
%span.ingredient
= ingredient.keys.first
%span.recipes
= ingredient.values.first.map{|recipe| recipe[1]}.join(", ")
At this point, I am well on my way to completing the template. Hopefully I can finish it off tomorrow.

1 comment: