Showing posts with label dry. Show all posts
Showing posts with label dry. Show all posts

Saturday, July 4, 2009

DRY RSS

‹prev | My Chain | next›

I have two RSS feeds, which look depressingly similar.

The meals RSS feed:
get '/main.rss' do
content_type "application/rss+xml"

url = "#{@@db}/_design/meals/_view/by_date?limit=10&descending=true"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Meals"
maker.channel.link = ROOT_URL
maker.channel.description = "Meals from a Family Cookbook"
@meal_view.each do |couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['key']}"
meal = JSON.parse(data)
date = Date.parse(meal['date'])
maker.items.new_item do |item|
item.link = ROOT_URL + date.strftime("/meals/%Y/%m/%d")
item.title = meal['title']
item.pubDate = Time.parse(meal['date'])
item.description = meal['summary']
end
end
end

rss.to_s
end
The recipes feed:
get '/recipes.rss' do
content_type "application/rss+xml"

url = "#{@@db}/_design/recipes/_view/by_date?limit=10&descending=true"
data = RestClient.get url
@recipe_view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Recipes"
maker.channel.link = ROOT_URL
maker.channel.description = "Recipes from a Family Cookbook"

@recipe_view.each do |couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['value'][0]}"
recipe = JSON.parse(data)
maker.items.new_item do |item|
item.link = ROOT_URL + "/recipes/recipe['id']"
item.title = recipe['title']
item.pubDate = Time.parse(recipe['date'])
item.description = recipe['summary']
end
end
end

rss.to_s
end
Whenever I DRY things up, I tend to over-generalize. I feel the need to remove the duplication and to prepare for as much future use as possible. Every time, I have to have an internal dialog with myself, so that I can be convinced that it is OK to simply eliminate duplication of business logic. I do not need to prevent future duplication—I can deal with that in future.

So, with pep talk in hand, I start with the recipe.rss feed. I replace the above with a call to a new helper, rss_for_date_view:
get '/recipes.rss' do
content_type "application/rss+xml"
rss_for_date_view
end
I create the rss_for_date_view helper and move the code that was previously in the recipe.rss handler into the new helper:
    def rss_for_date_view
url = "#{@@db}/_design/recipes/_view/by_date?limit=10&descending=true"
data = RestClient.get url
@recipe_view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Recipes"
maker.channel.link = ROOT_URL
maker.channel.description = "Recipes from a Family Cookbook"

@recipe_view.each do |couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['value'][0]}"
recipe = JSON.parse(data)
maker.items.new_item do |item|
item.link = ROOT_URL + "/recipes/recipe['id']"
item.title = recipe['title']
item.pubDate = Time.parse(recipe['date'])
item.description = recipe['summary']
end
end
end

rss.to_s
end
Now, I run my tests.

When refactoring, always run your tests (and you better have tests) with every change lest a change two moves prior compound to put the code into a completely unusable state.

When I run my specs, I get errors:
3)
NameError in 'GET /recipe.rss should request the 10 most recent meals from CouchDB'
uninitialized class variable @@db in Eee::Helpers
./helpers.rb:199:in `rss_for_date_view'
./eee.rb:68:in `GET /recipes.rss'
/home/cstrom/.gem/ruby/1.8/gems/sinatra-0.9.2/lib/sinatra/base.rb:779:in `call'
/home/cstrom/.gem/ruby/1.8/gems/sinatra-0.9.2/lib/sinatra/base.rb:779:in `route'
...
Ah, that is a fairly easy one. I do not have access to the @@db Sinatra class variable in the helpers. I created a stub-able _db helper method to work around that. After searching and replacing @@db for _db, all of my tests are passing again.

Before trying to get the rss_for_date_view helper working with meals, I take some time to rename recipe-specific variables to be more general, renaming "recipe_view" to be plain "view" and "recipe" to be "record":
    def rss_for_date_view
url = "#{_db}/_design/recipes/_view/by_date?limit=10&descending=true"
data = RestClient.get url
view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Recipes"
maker.channel.link = ROOT_URL
maker.channel.description = "Recipes from a Family Cookbook"

view.each do |couch_rec|
data = RestClient.get "#{_db}/#{couch_rec['value'][0]}"
record = JSON.parse(data)
maker.items.new_item do |item|
item.link = ROOT_URL + "/recipes/record['id']"
item.title = record['title']
item.pubDate = Time.parse(record['date'])
item.description = record['summary']

end
end
end

rss.to_s
end
And then I run my tests to ensure that I have not broken anything (I haven't—this time).

The primary difference between the meal and recipe feeds is the name—both in the title and in the CouchDB view. The name ("recipes" or "meals") is always plural, so I can introduce it as the first argument to the rss_for_date_view helper:
get '/recipes.rss' do
content_type "application/rss+xml"
rss_for_date_view("recipes")
end
And the updated helper:
    def rss_for_date_view(feed)
url = "#{_db}/_design/#{feed}/_view/by_date?limit=10&descending=true"
data = RestClient.get url
view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: #{feed.upcase}"
maker.channel.link = ROOT_URL
maker.channel.description = "#{feed.upcase} from a Family Cookbook"

view.each do |couch_rec|
data = RestClient.get "#{_db}/#{couch_rec['value'][0]}"
record = JSON.parse(data)
maker.items.new_item do |item|
item.link = ROOT_URL + "/recipes/record['id']"
item.title = record['title']
item.pubDate = Time.parse(record['date'])
item.description = record['summary']
end
end
end

rss.to_s
end
And then I run the tests.

This time I do get an error:
1)
'GET /recipe.rss should be the meals rss feed' FAILED
expected following output to contain a <channel title>EEE Cooks: Recipes</channel title> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<?xml version="1.0" encoding="UTF-8"?><html><body><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"><channel><title>EEE Cooks: RECIPES</title>
<link>http://www.eeecooks.com
<description>RECIPES from a Family Cookbook</description></channel></rss></body></html>
./spec/eee_spec.rb:467:
Whoops! It is capitalize, not upcase. Good thing I had those tests!

And again, I run all of the tests (now they pass).

The last reference to recipes in the rss_for_date_view helper is when calculating a link to the recipe for the RSS feed. Since I will need to calculate the link for both the recipe and meal RSS feeds, that means that I will have to pass a code block into the helper. Something like this:
get '/recipes.rss' do
content_type "application/rss+xml"
rss_for_date_view("recipes") do |rss_item, recipe|
rss_item.link = ROOT_URL + "/recipes/#{recipe['id']}"
end

end
And, to get the helper to use the code block, it needs to yield:
    def rss_for_date_view(feed)
url = "#{_db}/_design/#{feed}/_view/by_date?limit=10&descending=true"
data = RestClient.get url
view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: #{feed.capitalize}"
maker.channel.link = ROOT_URL
maker.channel.description = "#{feed.capitalize} from a Family Cookbook"

view.each do |couch_rec|
data = RestClient.get "#{_db}/#{couch_rec['value'][0]}"
record = JSON.parse(data)
maker.items.new_item do |item|
item.title = record['title']
item.pubDate = Time.parse(record['date'])
item.description = record['summary']

yield item, record
end
end
end

rss.to_s
end
With that, I am ready to replace the duplicate RSS code in the meal RSS action with this:
get '/main.rss' do
content_type "application/rss+xml"

rss_for_date_view("meals") do |rss_item, meal|
date = Date.parse(meal['date'])
rss_item.link = ROOT_URL + date.strftime("/meals/%Y/%m/%d")
end
end
Again, I run all of my tests. And this time, they pass.

Not too shabby, I replaced some horrid duplication with two much smaller code blocks:
get '/main.rss' do
content_type "application/rss+xml"

rss_for_date_view("meals") do |rss_item, meal|
date = Date.parse(meal['date'])
rss_item.link = ROOT_URL + date.strftime("/meals/%Y/%m/%d")
end
end

get '/recipes.rss' do
content_type "application/rss+xml"

rss_for_date_view("recipes") do |rss_item, recipe|
rss_item.link = ROOT_URL + "/recipes/#{recipe['id']}"
end
end
Best of all, I did it very quickly and with a high degree of assurance that nothing went wrong—thanks to my tests.
(commit)

Thursday, June 18, 2009

DRYing up the Categories

‹prev | My Chain | next›

I'm on vacation, but the don't-break-the-chain gods recognize no holidays and never spend a day at the beach. Lest they gaze upon me with disfavor, I will continue to do at least a little work each day on my chain to stay in their good graces. I have no idea what the wrath of the don't-break-the-chain gods is like and I do not intend to find out.

I noted last night that the code for the category links at the top of most pages was starting to feel repetitive. On the legacy Rails site, the category links look like:



The "Vegetarian" category is in red to indicate that the user is on a vegetarian meal, recipe, or list.

The new implementation of recipe categories, using Haml, looks like:
%ul#eee-categories
%li= recipe_category_link(@recipe, 'Italian')
%li= recipe_category_link(@recipe, 'Asian')
%li= recipe_category_link(@recipe, 'Latin')
%li= recipe_category_link(@recipe, 'Breakfast')
%li= recipe_category_link(@recipe, 'Chicken')
%li= recipe_category_link(@recipe, 'Fish')
%li= recipe_category_link(@recipe, 'Meat')
%li= recipe_category_link(@recipe, 'Salad')
%li= recipe_category_link(@recipe, 'Vegetarian')
%li
%a Recipes
There is much repetition in there—9 calls of the recipe_category_link helper, which is responsible for highlighting the link when appropriate. What's worse is that the following is in the meal Haml template (the above is in the recipe template):
%ul#eee-categories
%li= recipe_category_link(@recipes, 'Italian')
%li= recipe_category_link(@recipes, 'Asian')
%li= recipe_category_link(@recipes, 'Latin')
%li= recipe_category_link(@recipes, 'Breakfast')
%li= recipe_category_link(@recipes, 'Chicken')
%li= recipe_category_link(@recipes, 'Fish')
%li= recipe_category_link(@recipes, 'Meat')
%li= recipe_category_link(@recipes, 'Salad')
%li= recipe_category_link(@recipes, 'Vegetarian')
%li
%a Recipes
I have definitely reached the refactor portion of the Red-Green-Refactor behavior driven development cycle. I could DRY the code up by either factoring the duplication out into a shared partial or into a helper. I opt for the helper since recipe_category_link is already a helper—might as well keep them close should any later work need to be done.

The specs for the categories helper end up reading:
categories
- should link to the italian category
- should be able to highlight the link to the italian category
- should link to the fish category
- should link to the vegetarian category
- should link to all recipes
Since there is so much repetition in the the category listing, the individual RSpec examples are all very much the same. The "vegetarian" example reads:
  it "should link to the vegetarian category" do
categories({}).
should have_selector("#eee-categories a", :content => "Vegetarian")
end
The other examples replace "vegetarian" with the appropriate other category.

The categories helper gets driven to:
    def categories(context)
categories = %w{Italian Asian Latin Breakfast Chicken Fish Meat Salad Vegetarian}

links = categories.map do |category|
%Q|<li>#{recipe_category_link(context, category)}</li>|
end

links << "<a>Recipes</a>"

%Q|<ul id="eee-categories">#{links}</ul>|
end
Finally, I replace the repetitive Haml code with a single call to the new categories helper. I make sure to run all of my specs and then call it a night.
(commit)