Sinatra and Google Currency API - Part 2
13/Sep 2013
Sinatra and Google Currency API – Part 2
This guest post is by Girish Sonawane, a self-taught programmer. He came across Ruby in 2008 and has since been working full-time on Ruby. He worked as a Rails freelancer and later co-founded Cube Root, an exclusive Ruby on Rails software boutique catering to outsourced work. His interests are everything related to technology or science. You can reach him at girish@cuberoot.in or via twitter @girishso.
In this two-part series, I will show you how to use Google currency conversion API and use it in a small Sinatra app. In Part 1 we built a small function to access the Google API for currency conversion. This is Part 2, where we will build a small Sinatra app using the function we created in Part 1.
The source code for this series is available on Github, with commits for each step. This little library is available as a Ruby Gem at GoogCurrency.
Wireframes
We will have two screens, one with a form where a user can submit data and another showing the result.
Home page
Results page
Sinatra
Since this is a very basic app, we are going to use Sinatra in classic style. We need following files.
Gemfile
for all the gem dependencies.
1 source 'https://rubygems.org' 2 3 gem "rest-client", "1.6.7" 4 gem "json", "1.8.0" 5 gem "rspec", "2.14.1" 6 gem "fakeweb", "1.3.0" 7 gem "sinatra", "1.4.3" 8 gem "capybara", "2.1.0" 9 gem "haml"
config.ru
required to run the app on Rack servers like Passenger, Heroku etc.
1 require 'bundler' 2 Bundler.require 3 require './app' 4 run Sinatra::Application
Bundler.require
will require all the gems listed in our Gemfile and made available to our Sinatra app.
Let’s create an empty app.rb
, the main Sinatra app file with touch app.rb
Continuing with the same spirit as Part 1, we are going to test drive this app. We are going to use capybara
for acceptance tests.
spec/acceptance_spec.rb
1 require 'bundler' 2 Bundler.require 3 4 disable :run 5 set :root, File.dirname(__FILE__) + "/.." 6 require Sinatra::Application.root + '/app' 7 8 Capybara.app = Sinatra::Application 9 10 RSpec.configure do |config| 11 config.include Capybara::DSL 12 end 13 14 describe 'currency converter' do 15 it "loads currency converter form" 16 it "converts currencies" 17 it "handles errors" 18 end
Line 4, disables the web server, we don’t need a webserver for specs. Line 8, tells capybara
it’s a Sinatra app. Line 11, makes capybara
DSL available to our specs.
Line 15, 16, 17, adds three pending specs.
If we execute our spec now with rspec spec/acceptance_spec.rb
, it says:
1 *** 2 3 Pending: 4 currency converter loads currency converter form 5 # Not yet implemented 6 # ./spec/acceptance_spec.rb:15 7 currency converter converts currencies 8 # Not yet implemented 9 # ./spec/acceptance_spec.rb:16 10 currency converter handles errors 11 # Not yet implemented 12 # ./spec/acceptance_spec.rb:17
Let’s define the first spec and make it pass.
1 it "loads currency converter form" do 2 visit "/" 3 page.should have_content("Currency Converter") 4 find('form').should have_button('Convert') 5 end
capybara
simulates user interactions with the website. visit "/"
takes the user to the home page of the site as expected. Line 3, checks for existence of “Currency Converter” text on the page. Line 4, expects a form
with the Convert
button.
Typically, there is no need of such granular level testing, but this tells us if the test suite is working as expected.
Executing the spec fails with:
1 expected #has_content?("Currency Converter") to return true, got false
Now to make this spec pass, let’s modify app.rb
:
1 get "/" do 2 haml :"index" 3 end
get "/"
loads the home page. We are using haml
view templates instead of erb
. It expects views/index.haml
in views folder. Let’s add it.
1 %h1 Currency Converter 2 3 %form(action = "/convert" method = "post") 4 5 %input#convert(type="submit" value="Convert")
Let’s also add a layout views/layout.haml
:
1 !!! 2 %html 3 %head 4 %title Currency Conversion Tutorial 5 %body 6 = yield
Executing specs with rspec spec/acceptance_spec.rb
passes the spec. We are good to go with the next pending spec.
1 valid_response =<<-VALID 2 {lhs: "1 U.S. dollar",rhs: "54.836587 Indian rupees",error: "",icc: true} 3 VALID 4 . 5 . 6 . 7 it "converts currencies" do 8 FakeWeb.register_uri(:get, 9 "http://www.google.com/ig/calculator?hl=en&q=1USD=?INR", 10 :status => "200", 11 :body => valid_response) 12 visit '/' 13 14 fill_in "amount", :with => 1 15 select "USD", :from => "from" 16 select "INR", :from => "to" 17 click_button 'Convert' 18 19 find("#result").should have_content('54.836587') 20 end
We are again using fakeweb
gem to simulate the Google API interaction, Line 8.
We are simulating user converting 1 USD to INR, using capyabara
DSL to simulate the user interactions
Line 12, visit home page.
Line 14, Fill amount
input field with 1.
Line 15, select USD
from from
currencies select box.
Line 16, select INR
from to
currencies select box.
Line 18, click button “Convert”
Line 20, we expect to have #result
with the converted amount.
Executing spec fails with:
1 Unable to find field "amount"
Let’s go ahead and add the amount
field and other fields as well in index.haml
.
1 %h1 Currency Converter 2 3 %form(action = "/convert" method = "post") 4 %fieldset 5 %legend 6 From 7 %input#amount(name="amount") 8 9 %select#from(name="from") 10 %option(value="inr") INR 11 %option(value="usd") USD 12 %option(value="eur") EUR 13 14 To 15 %select#to(name="to") 16 %option(value="inr") INR 17 %option(value="usd") USD 18 %option(value="eur") EUR 19 20 %input#convert(type="submit" value="Convert")
Executing the spec now fails with:
1 Unable to find css "#result"
To fix this, we need to add post "/convert"
handler in app.rb
:
1 post "/convert" do 2 @result = GoogCurrency.send("#{params[:from]}_to_#{params[:to]}".to_sym, params[:amount]) 3 haml :"convert" 4 end
Line 2, we are generating the GoogCurrency method to call dynamically. params[:form]
, params[:to]
have the from and to currencies respectively. params[:amount]
has the amount to convert. "#{params[:from]}_to_#{params[:to]}"
gets converted to usd_to_inr
in our case. But how do we invoke this method? In Ruby, we don’t invoke methods, we send a message to the object and the object responds to the message. To invoke this method we send
message usd_to_inr
to GoogCurrency
, along with the method parameter (amount
).
Then we render haml
template convert
.
convert.haml
1 %div 2 #{params[:amount]} #{params[:from]} = 3 %span#result= @result 4 #{params[:to]} 5 6 %a(href="/") Back
The spec now passes.
Now, we only have one more spec left i.e. “currency converter handles errors”. Let’s get at it.
1 it "handles errors" do 2 invalid_response =<<-INVALID 3 {lhs: "",rhs: "",error: "4",icc: false} 4 INVALID 5 FakeWeb.register_uri(:get, 6 "http://www.google.com/ig/calculator?hl=en&q=xyzUSD=?INR", 7 :status => "200", 8 :body => invalid_response) 9 10 visit '/' 11 12 fill_in "amount", :with => "xyz" 13 select "USD", :from => "from" 14 select "INR", :from => "to" 15 click_button 'Convert' 16 17 find("#error").should have_content("An error occurred: 4") 18 end
It’s similar to the earlier spec, but Fakeweb
now returns an error response, because the amount
is invalid. Executing the spec now fails with:
1 Unable to find css "#error"
To make this spec pass, let’s handle the exception raised by GoogCurrency
in app.rb
:
1 post "/convert" do 2 begin 3 @result = GoogCurrency.send("#{params[:from]}_to_#{params[:to]}".to_sym, params[:amount]) 4 rescue Exception => ex 5 @error = ex.message 6 end 7 haml :"convert" 8 end
Here, we’re rescuing the exception and setting @error
instance variable.
Now in convert.haml
, let’s display the error message.
1 -if @error 2 #error= @error 3 -else 4 %div 5 #{params[:amount]} #{params[:from]} = 6 %span#result= @result 7 #{params[:to]} 8 9 %a(href="/") Back
All the specs now pass. Note, we haven’t opened the browser manually even once! Let’s do it and hope everything is fine and dandy! Execute the command rackup -p 4567
and visit localhost:4567
.
That’s it!
Feel free to ask questions and give feedback in the comments section of this post. Thanks!