I have two RSS feeds, which look depressingly similar.
The meals RSS feed:
get '/main.rss' doThe recipes feed:
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
get '/recipes.rss' doWhenever 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.
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
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' doI create the
content_type "application/rss+xml"
rss_for_date_view
end
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_viewNow, I run my tests.
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
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)Ah, that is a fairly easy one. I do not have access to the
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'
...
@@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_viewAnd then I run my tests to ensure that I have not broken anything (I haven't—this time).
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
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' doAnd the updated helper:
content_type "application/rss+xml"
rss_for_date_view("recipes")
end
def rss_for_date_view(feed)And then I run the tests.
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
This time I do get an error:
1)Whoops! It is
'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:
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' doAnd, to get the helper to use the code block, it needs to yield:
content_type "application/rss+xml"
rss_for_date_view("recipes") do |rss_item, recipe|
rss_item.link = ROOT_URL + "/recipes/#{recipe['id']}"
end
end
def rss_for_date_view(feed)With that, I am ready to replace the duplicate RSS code in the meal RSS action with this:
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
get '/main.rss' doAgain, I run all of my tests. And this time, they pass.
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
Not too shabby, I replaced some horrid duplication with two much smaller code blocks:
get '/main.rss' doBest of all, I did it very quickly and with a high degree of assurance that nothing went wrong—thanks to my tests.
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
(commit)
No comments:
Post a Comment